Files
everything-claude-code/scripts/hooks
Jamkris 231c1fdbe8 fix: close gateguard destructive-bash regex bypasses with tokenizer
Six classes of bypass in scripts/hooks/gateguard-fact-force.js
DESTRUCTIVE_BASH regex, plus a separate false-positive class.
Same shape of issue as the block-no-verify holes addressed in
#1843: a single-regex shell parser can never cover the flag-order
variations git and rm allow.

Real bypasses observed locally (all ALLOW today, should BLOCK):

  git push -f origin main                       (short form of --force)
  git -c core.foo=bar reset --hard              (intervening -c global)
  rm -fr /tmp/junk                              (reverse flag order)
  rm -r -f /tmp/junk                            (split flag form)
  git reset HEAD --hard                         (intervening ref token)
  git clean -fd                                 (combined -f + -d flag)

False positive observed locally (BLOCK today, should ALLOW):

  git commit -m "fix: rm -rf race in worker"   (destructive phrase
                                                inside quoted message)

Behavior fix that comes along: --force-if-includes is now exempted
alongside --force-with-lease. Both are safety-checked variants;
the previous regex used a negative lookahead that only spelled out
--with-lease, so --force-if-includes blocked under the old code
even though it is the safer-not-harder choice.

Fix shape (mirrors block-no-verify #1843):

  - DESTRUCTIVE_SQL_DD regex kept for `drop table`, `delete from`,
    `truncate`, `dd if=` — these are stable keyword phrases. Quoted
    strings are stripped before the regex runs so the phrase is not
    matched inside a commit message body.
  - isDestructiveBash() tokenizes the command into segments at
    unquoted ; | & boundaries, then per segment:
      * isDestructiveRm  — detects `rm` with both r and f set
        across combined or split flag tokens.
      * isDestructiveGit — finds the git subcommand after skipping
        global options (-c key=val, -C path, --git-dir=, etc.),
        then handles reset, checkout --, clean -f*, push --force
        (with --force-with-lease / --force-if-includes exemption),
        commit --amend, and rm -r* preservation.
  - Command tokens go through commandBasename() so /usr/bin/rm,
    rm.exe, and RM all normalize to "rm".

Tests (+10 in tests/hooks/gateguard-fact-force.test.js):

  Bypass-now-blocked (7):
    - denies short-form git push -f
    - denies git reset --hard with intervening -c global option
    - denies rm -fr (reverse flag order)
    - denies rm -r -f (split flag form)
    - denies git reset HEAD --hard
    - denies git clean -fd
    - denies destructive command in second chained segment

  False-positive-now-allowed (3):
    - allows destructive phrase inside `-m` commit message (rm -rf)
    - allows SQL phrase inside `-m` commit message (drop table)
    - allows --force-if-includes as a safety-checked variant

Local verification:
  yarn lint                                                 clean
  scripts/ci/validate-*  (agents/commands/rules/skills/hooks/
                          install-manifests/no-personal-paths)  pass
  node tests/run-all.js                              2380/2380 pass

Caveat (unrelated): yarn test still fails at check-unicode-safety
on skills/windows-desktop-e2e/SKILL.md (U+2605) per #1843's
caveat — independent of this change.

Provenance: discovered during a security pass on ECC after PR
#1843 (block-no-verify shell-words rewrite) landed. Same class of
regex-based shell parser issue, same shape of fix.

Refs #1843.
2026-05-13 14:54:27 +09:00
..