ClaudeMods
☰
JA
● 0 人がオンライン ・閲覧 0 回
スポンサー作品を投稿
GitHub リポジトリ · 投稿者 hughescr

git-guard

プロセス内の `tool.call` mod で、未コミットの作業を黙って破棄する Bash の git コマンドを拒否します。`hooks/git-guard.sh` を mod 化したもので、切り替え後はこのスクリプトに置き換わり、各 Bash 呼び出しでスクリプトが発生させていた shell fork(ここでは約 34 ms、こちらの基準では 1 マイクロ秒未満)を省けます。

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、こちらの基準では 1 マイクロ秒未満)を省けます。

状態: ステージングのみ。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 | あらゆる dry run(-n、--dry-run、--force 付きも含む) |

拒否理由はすべて次の文で終わります:「破棄するのではなく編集して、自分の変更を自分で取り消してください。この破壊的なコマンドが本当に必要なら Craig に例外を依頼してください。」空のコマンドや文字列でないコマンドはそのまま通過します。tool.call はサブエージェントにも適用されるため、サブエージェント用の除外はありません。

判定方法

hooks/guard.ts は純粋な文字列ロジックです(claude-code の import はありません)。shell スクリプトの [[:space:]] と \b は Unicode プロパティではなく BSD grep と libc の意味論なので、文字クラスをそのまま再現します。

  • [[: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 のタイムアウトを超えて fail open になっていました。現在は 1 MB の敵対的な入力でも 100 ms を大きく下回る時間で完了します。

残された穴(スクリプトとの parity。後で別の明示的な変更として修正)

  • コマンドのどこかに --staged があれば git restore を許可します。-S は認識しないため、git restore -S f は拒否されます。
  • dry-run のような 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 の golden corpus、拒否理由のテキスト、1 MB の性能ケース)。元のスクリプトと mod を同じ corpus とランダム fuzz に対して実行し、shell スクリプトとの parity を証明済みです。

インストール

まず作者の 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.

関連作品