Skip to content

block-hook-bypass.sh:375 — patsub_replacement (Bash 5.2+) silently disables the >&N sentinel restore, false-positives on bare echo/printf >&2 #1667

Description

@kyle-sexton

Context

Produced by plugin-quality:audit after two live block events during an otherwise-successful
autonomous session. Audited version: 0.17.3 (active install). This is the one finding in this
audit batch with a direct, reproduced empirical confirmation, not a static-review inference.

Finding (IMPORTANT, empirically proven)

hooks/block-hook-bypass.sh:375 — the fd-duplication redirect sentinel restore silently no-ops
under Bash's patsub_replacement shopt (default-on since Bash 5.2), causing a false-positive block
on a bare echo/printf ... >&N (no leading digit) or &>target redirect with no accompanying
real file write.

The line:

normalized="${normalized//"$soh"/&}"

is meant to restore a sentinel character back to & after the &-as-separator split, so a
fd-duplication redirect like >&2 survives the segment split as one token and is correctly
recognized as NOT a real file write. Under Bash's patsub_replacement shopt (on by default since
Bash 5.2, which is what this marketplace's own CI runs — ubuntu-24.04, Bash 5.2.21), this specific
substitution form silently no-ops, so the sentinel is never restored. The result: a completely
benign echo "some diagnostic" >&2 gets misclassified as a real file-write bypass and the whole
Bash tool call is blocked.

Reproduced two ways: (1) a same-process controlled experiment toggling patsub_replacement on
vs. off against otherwise-identical code, and (2) direct invocation of the hook with the exact raw
command that triggered the block live (a multi-line telemetry-upsert script containing
echo "..." >&2 and an unrelated gh api ... >/dev/null). Both confirm the false positive
definitively.

Test-suite gap, not an environment mismatch. block-hook-bypass.test.sh passes 203/203 on the
exact same Bash build that has the bug, and the plugin's own CI runs on that same affected version
range. The suite simply never isolates a standalone >&N/&> redirect with no accompanying real
file write in the same segment — every existing test case pairs a real write with something else,
so this exact shape was never exercised.

Blast radius contained: this ${var//pat/&} anti-pattern appears exactly once in this plugin
(line 375); the shared marketplace-wide hook-utils.sh single-source-of-truth helper does not use
this pattern, so other hooks built on it are not affected.

Confirmed clean — not findings, recorded so they aren't re-litigated

  • Permission-invisibility claim confirmed correct (fetched 2026-07-26,
    https://code.claude.com/docs/en/hooks): PreToolUse exit 2 blocks prior to, and independent
    of, Claude Code's permission system — no permissions.allow/autoMode.allow entry can prevent
    this class of block. This means the earlier ask ("what permission prompts should go on the
    allow-list") does not apply to this friction point at all — it needs a code fix, not a config
    change.
  • Kill switch is properly documented: block_hook_bypass_enabled appears in both plugin.json's
    userConfig schema and the README's kill-switch table.
  • Bash/PowerShell routing correctly ruled out as a factor, and the POSIX-style /tmp/... path
    in the triggering command is inert to the trace (Git Bash on Windows resolves it fine; not
    Windows-path-specific).

Suggested remediation order (cheapest → most ambitious)

  1. Replace the buggy substitution with a form immune to patsub_replacement
    (e.g. an escaped \& literal, or a loop-based restore) — needs empirical verification on Bash
    versions older than 5.2 before shipping, since the replacement form's behavior may differ there
    too.
  2. Add a regression test case: a standalone echo ... >&2 / printf ... >&1 / &>target with no
    real file write anywhere else in the command, run explicitly under both patsub_replacement on
    and off, so this exact gap can't reopen silently on a future Bash version bump.
  3. Broader (optional, out of scope for this specific bug): a grep sweep of the marketplace's other
    guardrails hooks for the same ${var//pat/&} anti-pattern, since this instance was found by
    targeted search rather than an exhaustive one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.priority: highSignificant impact, or blocks an imminent release; staff this cycle.status: needs-decisionAwaiting a human or maintainer judgment call.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions