ClaudeMods
☰
ZH-TW
● 0 人在線上 · 瀏覽 0 次
贊助提交作品
GitHub 儲存庫 · 發布者 hughescr

git-guard

一個行程內的 `tool.call` mod,會拒絕會靜默丟棄未提交工作的 Bash git 命令。它是 `hooks/git-guard.sh` 的 mod 版本,切換完成後可取代該腳本,省下腳本在每次 Bash 呼叫產生的 shell fork(此處約 34 ms,而本機低於一微秒)。

hughescr@hughescr

hughescr/claude-code-config/tree/develop/my-plugins/git-guard

已翻譯

關於這個 mod

git-guard

一個行程內的 tool.call mod,會拒絕會靜默丟棄未提交工作的 Bash git 命令。它是 hooks/git-guard.sh 的 mod 版本,切換完成後可取代該腳本,省下腳本在每次 Bash 呼叫產生的 shell fork(此處約 34 ms,而本機低於一微秒)。

狀態: 僅已暫存。在 Craig 作出其他決定前,settings.json 中執行 hooks/git-guard.sh 的 PreToolUse Bash 項目仍是正式的強制措施。此外掛在安裝前不會啟用(先執行 marketplace update,再執行 install)。啟用、驗證與回滾請見:my-plugins/MODS-ACTIVATION.md。

它會拒絕什麼

透過 git 全域選項比對(git -C dir ...、git -c k=v ...、--git-dir、--work-tree、--namespace、 --exec-path、--super-prefix=、--config-env=、-p、--paginate、-P、--no-pager、--no-optional-locks、 --no-replace-objects、--literal-pathspecs、--glob-pathspecs、--noglob-pathspecs、--icase-pathspecs、 --bare):

| 命令 | 允許的例外 | |---|---| | git checkout ... -- <path>(checkout 後是獨立的 -- token,中間沒有 \|&;) | git checkout main、-b、--track | | git restore | 純 --staged 形式(任何位置都沒有 --worktree 或 -W) | | 命令中任何位置出現完整 token --hard 的 git reset | --soft、--mixed、--hard-not-really | | 帶強制旗標(-f、-fd、-fdx、--force)的 git clean | 任何試跑(-n、--dry-run,即使同時有 --force) |

每個拒絕原因最後都會附上:「透過編輯來撤銷你自己的修改,不要丟棄它們;如果確實需要這個破壞性命令,請向 Craig 請求例外。」空命令或非字串命令會直接通過。tool.call 對子代理同樣生效,因此沒有子代理篩選器。

它如何比對

hooks/guard.ts 是純字串邏輯(不匯入 claude-code)。shell 腳本的 [[:space:]] 與 \b 遵循 BSD grep 和 libc 語意,而不是 Unicode 屬性,因此字元類別會原樣重現:

  • [[:space:]] 是去除 U+FEFF 後的 JS \s(24 個碼點)。
  • 關鍵字後的 \b 使用 hooks/wordchars.ts;這是本機 BSD grep 視為單字字元的產生表(706 個範圍)。tests/gen-git-guard-wordchars.ts 會重新產生它(從 ~/.claude 執行 bun my-plugins/git-guard/tests/gen-git-guard-wordchars.ts);只有在 macOS 升級後、且 hooks/git-guard.sh 仍存在作為參考時才重新產生。
  • git ... <subcommand> 前綴逐一掃描 token,維持線性時間。shell 的正規表示式會發生二次方回溯(重複 git -C 5,700 次、約 40 KB 的輸入耗時 5 s),這會跨過 hook 逾時並導致放行。現在即使是 1 MB 的對抗性輸入,也能在遠低於 100 ms 內完成。

保留的漏洞(與腳本一致,稍後在獨立標記的修改中修正)

  • 命令中任何位置出現 --staged 都會允許 git restore;不辨識 -S,所以 git restore -S f 會被拒絕。
  • 任何位置出現類似試跑的 token(即使是 -name)都會允許 git clean。
  • 未知的全域選項,或 git -C reset ...(其值吞掉了 reset),會漏過檢查。--git-dir= 使用空值時會破壞選項鏈。
  • git -c clean.requireForce=false clean -d 會被允許。
  • 不防護 shell 混淆(變數、引號技巧、eval、在腳本內執行 git)。
  • 仍有過度拒絕:git reset HEAD~1; echo --hard 和 git clean -d; rm -f x 會被拒絕。

刻意的行為變更

  1. shell 的 set -o pipefail 搭配 echo | grep -q,在約 64 KiB 以上的命令上可能因 SIGPIPE 偶爾漏掉比對。mod 的行為是確定的。
  2. 非字串命令會直接通過(在 Bash schema 下無法到達)。

測試

claude plugin test my-plugins/git-guard 會執行 tests/guard.test.ts(透過 $.tool.call 執行 tests/corpus.ts 中的黃金語料、拒絕原因文字,以及 1 MB 效能案例)。透過讓原始腳本和 mod 在同一份語料加隨機 fuzz 上執行,已證明兩者保持一致。

安裝

請先查看作者 README,確認 marketplace 與外掛名稱;指令可能隨儲存庫結構而變動。

claude plugin marketplace add hughescr/claude-code-config
claude plugin install git-guard
原文 / README

git-guard

An in-process tool.call mod that denies Bash git commands which silently discard uncommitted work. It is the mod form of hooks/git-guard.sh and replaces it once cut over, saving the shell forks that script cost on every Bash call (about 34 ms per call, against under a microsecond here).

Status: staged only. Until Craig decides otherwise, the PreToolUse Bash entry in settings.json that runs hooks/git-guard.sh stays the enforcement of record. This plugin is inert until it is installed (marketplace update, then install). Activation, verification and rollback: my-plugins/MODS-ACTIVATION.md.

What it denies

Matched through git global options (git -C dir ..., git -c k=v ..., --git-dir, --work-tree, --namespace, --exec-path, --super-prefix=, --config-env=, -p, --paginate, -P, --no-pager, --no-optional-locks, --no-replace-objects, --literal-pathspecs, --glob-pathspecs, --noglob-pathspecs, --icase-pathspecs, --bare):

| Command | Allowed exceptions | |---|---| | git checkout ... -- <path> (a bare -- token after checkout, no \|&; between) | git checkout main, -b, --track | | git restore | the pure --staged form (no --worktree or -W anywhere) | | git reset with --hard as a whole token anywhere in the command | --soft, --mixed, --hard-not-really | | git clean with a force flag (-f, -fd, -fdx, --force) | any dry run (-n, --dry-run, even with --force) |

Every deny reason ends with: "Undo your own edits by editing instead of discarding them; ask Craig for an exception if this destructive command is genuinely needed." A command that is empty or not a string passes through. tool.call fires for subagents as well, so there is no subagent filter.

How it matches

hooks/guard.ts is pure string logic (no claude-code imports). The shell script's [[:space:]] and \b are BSD grep and libc semantics, not Unicode properties, so the character classes are reproduced exactly:

  • [[:space:]] is JS \s minus U+FEFF (24 code points).
  • \b after a keyword uses hooks/wordchars.ts, a generated table of the characters BSD grep treats as word characters on this machine (706 ranges). tests/gen-git-guard-wordchars.ts regenerates it (bun my-plugins/git-guard/tests/gen-git-guard-wordchars.ts from ~/.claude); rerun it only after a macOS upgrade, while hooks/git-guard.sh still exists as the reference.
  • The git ... <subcommand> prefix is scanned token by token in linear time. The shell's regexes backtracked quadratically (git -C repeated 5,700 times, about 40 KB, took 5 s), which would have crossed the hook timeout and failed open. Every 1 MB adversarial input now finishes in well under 100 ms.

Preserved holes (parity with the script, to fix later in a separate, flagged change)

  • --staged anywhere in the command allows git restore; -S is not recognised, so git restore -S f is denied.
  • A dry-run-like token anywhere (even -name) allows git clean.
  • An unknown global option, or git -C reset ... (the value swallows reset), slips through. --git-dir= with an empty value breaks the option chain.
  • git -c clean.requireForce=false clean -d is allowed.
  • No defence against shell obfuscation (variables, quoting tricks, eval, scripts that run git inside).
  • Over-denies remain: git reset HEAD~1; echo --hard and git clean -d; rm -f x are denied.

Deliberate behaviour changes

  1. The shell's set -o pipefail with echo | grep -q could flakily miss a match on commands over about 64 KiB (SIGPIPE). The mod is deterministic.
  2. A non-string command passes through (unreachable under the Bash schema).

Tests

claude plugin test my-plugins/git-guard runs tests/guard.test.ts (the golden corpus in tests/corpus.ts through $.tool.call, deny-reason text, and 1 MB performance cases). Parity with the shell script was proven by running the original script and the mod over the same corpus plus random fuzz.

更多類似作品