ClaudeMods
☰
KO
● 0 명 접속 중 · 조회 0 회
후원프로젝트 제출
GitHub 저장소 · 작성자 hughescr

git-guard

프로세스 내부에서 동작하는 `tool.call` mod로, 커밋하지 않은 작업을 조용히 버리는 Bash git 명령을 거부합니다. `hooks/git-guard.sh`를 mod 형태로 옮긴 것이며, 전환이 끝나면 스크립트를 대체해 Bash 호출마다 스크립트가 만들던 셸 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 호출마다 스크립트가 만들던 셸 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 없음)。셸 스크립트의 [[: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 단위로 선형 시간에 스캔합니다. 셸 정규식은 이차적으로 백트래킹했습니다(git -C 를 5,700번 반복한 약 40 KB 입력에 5 s가 걸림)。이 때문에 hook 시간 제한을 넘겨 fail open 될 수 있었습니다. 이제 1 MB의 공격적인 입력도 100 ms보다 훨씬 짧은 시간에 끝납니다.

남아 있는 허점(스크립트와 동일하며, 나중에 별도의 표시된 변경으로 수정)

  • 명령 어디에든 --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는 허용됩니다.
  • 셸 난독화(변수, 인용부호 트릭, eval, 내부에서 git을 실행하는 스크립트)는 방어하지 않습니다.
  • 과도한 거부는 남아 있습니다. git reset HEAD~1; echo --hard와 git clean -d; rm -f x는 거부됩니다.

의도한 동작 변경

  1. 셸의 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에 실행해 셸 스크립트와의 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.

비슷한 프로젝트