Skip to content

v4.14.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 17:03
· 91 commits to main since this release

v4.14.0 — the command gate could be bypassed by choosing another shell tool

Full entry: CHANGELOG.md § [4.14.0]. This file records release evidence and the upgrade note.

Security fix. Reported by the paperclip+hermes-v1 consumer as escalation E6.

Existing installs are still exposed until you re-merge. Run
bash hooks/local/post-fusebase-update.sh --wire-hooks. Upgrading the tree alone does not
update your .claude/settings.json matcher.

Read this first — what is and is not fixed

This release makes PowerShell reach the gate. It does not make the rules PowerShell-shaped.

Gated now git reset --hard, git push --force, --no-verify, npx prisma migrate deploy, fusebase deploy — anything a shell-agnostic pattern already matched
Still open Remove-Item -Recurse -Force, ri -r -fo, … | Remove-Item -Recurse
Degraded, not denied rm -Recurse -Force matches destructive_file_delete (approval) rather than the FR-06 hard deny

Writing PowerShell cmdlet patterns is rule authoring, blocked on decisions D1/D2 in
docs/backlog/command-gate-shell-evasion/. That gap was hypothetical while the tool was unreachable
by the gate. It is reachable now, and the backlog entry records that change of status.

Measured coverage lives in docs/hook-coverage.md so "PowerShell is gated" cannot be over-read.

The defect

On Windows, Claude Code exposes a PowerShell tool beside Bash. Flow's command gate never saw it:

  • hooks/handlers/pre_tool_use.pyBASH_LIKE_TOOLS = {"Bash", "Shell", "Terminal", "ExecuteCommand"}, no PowerShell.
  • .claude/settings.json.example — matcher Bash|Edit|Write|MultiEdit|NotebookEdit, so the event never reached the handler at all.

Every FR-06 hard deny, every FR-12 require_approval, and the K4 policy-error fail-closed was
bypassable by choosing the other shell tool. Protected-path and secret arms were unaffected — they
key on Edit/Write.

A second, narrower bypass was found during the fix. hooks/handlers/permission_request.py
carried its own inline set — {"bash", "shell", "terminal"} — missing PowerShell and
ExecuteCommand. Its fall-through is ask, so an FR-06 hard deny was silently degrading to an
operator prompt rather than to allow. Measured: ExecuteCommand + npx prisma migrate deploy
ask before, deny/FR-12 after.

Concrete exposure, measured on the maintainer host across ~400 transcripts: 1,104 PowerShell
tool calls
, carrying production-touching commands such as
fusebase isolated-store sql bundle --app apps/admin-area --env prod. Not hypothetical traffic.

The fix

  • One shared set. hooks/shared/command_policy.py gains COMMAND_TOOL_NAMES + is_command_tool(); both handlers import it. The two divergent copies are gone — divergence was the second bypass.
  • All matcher carriers named. .claude/settings.json.example, .codex/hooks.json.example, DEFAULT_EVENT_MATCHERS, and the upstream-discovery path that reads the example file.
  • Re-merge widens an existing matcher. New _widen_matchers() unions Flow's required tokens into an already-installed matcher instead of only writing on fresh installs. It touches only the block whose chain names a hooks/handlers/ command, so consumer tools and foreign hook blocks survive. Idempotent.
  • Three RED-first fixtures freeze the tool set: two pre_tool_use, one permission_request. Driven through a worktree at the pre-fix commit — all three allow/ask before, denied after. Real discriminators, not outcome-only assertions.

Upgrade note

Situation What happens
Fresh install Gets the widened matcher automatically
Existing install, runs --wire-hooks Matcher widened in place; foreign blocks untouched
Existing install, does not re-wire Stays exposed. Nothing forces the re-merge
Hand-written regex matcher (^(Bash|Edit)$) Never rewritten — a naive split would corrupt it. The merge prints a warning naming the matcher; edit it by hand
* or absent matcher Left alone; already matches every tool

Coverage beyond this host

PowerShell is the only command-execution carrier besides Bash observable here. No Shell,
Terminal, ExecuteCommand, pwsh or Cmd appeared in live traffic — those names remain in the
set as defence in depth.

Tool names on hosts that cannot be observed from here — Cursor (run_terminal_cmd), Cline/Roo
(execute_command), Codex (local_shell) — were deliberately not added speculatively. Membership
is now case-insensitive and lives in one line of one file, with a tripwire recording that widening is
cheap and omission is the hole.

Release evidence

Item Value
Tag verification the v4.14.0 run is the authoritative gate; a local run is never release evidence
Pre-tag verification 8d5f99e green on both platforms
Suite 166/166; handler fixtures 26/26 with subprocess parity

Known residual — not fixed here

The EOL guard's printed remedy does not work. stamp-hook-manifest.sh correctly refuses on CRLF in
an eol=lf file and advises git add --renormalize -- <files>, but when the index is already LF that
is a no-op and the guard refuses again identically. A maintainer following the advice loops. The
working repair is rewriting the worktree from the index blob. Filed separately.