Description
Tighten guard so it stops interrupting work it was never meant to guard. Four
changes to plugins/guard/scripts/guard_hook.py (Claude Code only):
-
Stop no longer judges guard's own control-command turns. /guard:turn and
/guard:mode open a real transcript turn whose response is a one-line relay
("guard on"); the Stop evidence judge audited it and blocked it as an
"unsupported technical claim". The approval classifier already skipped control
commands at UserPromptSubmit, but cmd_stop had no matching skip. Fixed by
extracting the turn's command from the transcript's expanded
<command-name>/guard:turn</command-name> (_turn_command_name) and skipping
the audit for control commands. Applies to both headless and subagent modes.
-
New exempt_skills config (default []). Skills / slash commands whose
turn the Stop judge must not audit — named with their plugin namespace
(plugin:skill, e.g. guard:turn, hindsight:review) or bare for
un-namespaced skills. A user-invoked skill reaches the transcript as a
namespaced <command-name>, so the same command_name path handles both.
-
The approval gate exempts git-ignored writes. The PreToolUse gate denied
every unapproved Write/Edit except .claude/guard/refs/, so it blocked
writes that do not touch tracked project source: /handoff docs written to
.handover/, local config (**/*.local.*), and scratch/temp files. The gate
now lets a write through when git check-ignore reports the target ignored
(honors the global gitignore too). Guard's OWN config (.claude/guard.local.json)
and state tree (.claude/guard/) are excluded from this exemption
(_is_guard_owned) — both are git-ignored, so without the exclusion the model
could Write state/<sid>.json to self-arm approval or edit guard.local.json
to disable the judge. refs/ remains the one deliberate narrow allow.
-
Approval re-lock narrowed. The classifier revoked approval whenever a
message "opened a new/undecided topic" (opens_new_discussion), so a
mid-implementation question or refinement re-locked the gate. Renamed the signal
to starts_unrelated_task and rewrote APPROVAL_SYSTEM so approval is revoked
only when the user clearly pivots to a different, unrelated task — questions,
clarifications, refinements, corrections, and continuations keep it in place.
Context
Reported behavior: the evidence judge blocked a /guard:turn on relay, and the
gate "blocked writes too often" — specifically skill-authored docs, test-time
local config, and files in git-ignored folders, which are not the user's task
edits. Root evidence for (1) is session b30dbaec (a /guard:turn on relay was
blocked with "guard: finish the work before stopping").
PreToolUse carries transcript_path + prompt_id (official Claude Code hooks
reference), but the git-ignore rule made reading the transcript in the gate
unnecessary. Verified on Claude Code v2.1.197.
Docs updated alongside: plugins/guard/AGENTS.md, dev/design.md, README.md,
guard.local.json.example.
Acceptance Criteria
- Stop skips control/exempt turns — verified live (v2.1.197): typing
/guard:turn on produced trace stop … skip_exempt_skill command=guard:turn
with no block; a normal "say hello" turn produced stop … pass.
exempt_skills matching honors the plugin namespace — verified: with
exempt_skills:["hindsight:review"], a /hindsight:review turn is skipped;
guard:turn is skipped even with an empty list.
- Gate exempts git-ignored writes but never guard's own files — verified by a
direct 8-case matrix against the real repo + git check-ignore: tracked source
(guard_hook.py, AGENTS.md) → DENY; .handover/*, .claude/settings.local.json,
*.local.json → allow_gitignored; .claude/guard/state/*.json and
.claude/guard.local.json → DENY.
- Gate behavior live (v2.1.197,
acceptEdits, approval False): tracked
probe-tracked.txt → gate deny (not created); git-ignored .handover/probe.txt
→ gate allow_gitignored (created).
- Re-lock narrowed — verified with the real haiku classifier: "진행해" → arm;
"근데 이 함수 왜 이렇게 짰어?" and "변수명 더 명확하게 바꾸는 게 낫지 않아?" → no
re-lock; "여기 테스트도 추가해줘" → arm; "그건 됐고, 이제 다른 결제 모듈 논의하자"
→ re-lock. End-to-end: approved=True stays True through a question, flips False on
the unrelated task.
python3 -c "import ast; ast.parse(...)" passes; no stale opens_new/
exempt_commands references remain.
Description
Tighten guard so it stops interrupting work it was never meant to guard. Four
changes to
plugins/guard/scripts/guard_hook.py(Claude Code only):Stop no longer judges guard's own control-command turns.
/guard:turnand/guard:modeopen a real transcript turn whose response is a one-line relay("guard on"); the Stop evidence judge audited it and blocked it as an
"unsupported technical claim". The approval classifier already skipped control
commands at
UserPromptSubmit, butcmd_stophad no matching skip. Fixed byextracting the turn's command from the transcript's expanded
<command-name>/guard:turn</command-name>(_turn_command_name) and skippingthe audit for control commands. Applies to both
headlessandsubagentmodes.New
exempt_skillsconfig (default[]). Skills / slash commands whoseturn the Stop judge must not audit — named with their plugin namespace
(
plugin:skill, e.g.guard:turn,hindsight:review) or bare forun-namespaced skills. A user-invoked skill reaches the transcript as a
namespaced
<command-name>, so the samecommand_namepath handles both.The approval gate exempts git-ignored writes. The
PreToolUsegate deniedevery unapproved
Write/Editexcept.claude/guard/refs/, so it blockedwrites that do not touch tracked project source:
/handoffdocs written to.handover/, local config (**/*.local.*), and scratch/temp files. The gatenow lets a write through when
git check-ignorereports the target ignored(honors the global gitignore too). Guard's OWN config (
.claude/guard.local.json)and state tree (
.claude/guard/) are excluded from this exemption(
_is_guard_owned) — both are git-ignored, so without the exclusion the modelcould
Writestate/<sid>.jsonto self-arm approval or editguard.local.jsonto disable the judge.
refs/remains the one deliberate narrow allow.Approval re-lock narrowed. The classifier revoked approval whenever a
message "opened a new/undecided topic" (
opens_new_discussion), so amid-implementation question or refinement re-locked the gate. Renamed the signal
to
starts_unrelated_taskand rewroteAPPROVAL_SYSTEMso approval is revokedonly when the user clearly pivots to a different, unrelated task — questions,
clarifications, refinements, corrections, and continuations keep it in place.
Context
Reported behavior: the evidence judge blocked a
/guard:turn onrelay, and thegate "blocked writes too often" — specifically skill-authored docs, test-time
local config, and files in git-ignored folders, which are not the user's task
edits. Root evidence for (1) is session
b30dbaec(a/guard:turn onrelay wasblocked with "guard: finish the work before stopping").
PreToolUse carries
transcript_path+prompt_id(official Claude Code hooksreference), but the git-ignore rule made reading the transcript in the gate
unnecessary. Verified on Claude Code v2.1.197.
Docs updated alongside:
plugins/guard/AGENTS.md,dev/design.md,README.md,guard.local.json.example.Acceptance Criteria
/guard:turn onproduced tracestop … skip_exempt_skill command=guard:turnwith no block; a normal "say hello" turn produced
stop … pass.exempt_skillsmatching honors the plugin namespace — verified: withexempt_skills:["hindsight:review"], a/hindsight:reviewturn is skipped;guard:turnis skipped even with an empty list.direct 8-case matrix against the real repo +
git check-ignore: tracked source(
guard_hook.py,AGENTS.md) → DENY;.handover/*,.claude/settings.local.json,*.local.json→ allow_gitignored;.claude/guard/state/*.jsonand.claude/guard.local.json→ DENY.acceptEdits, approval False): trackedprobe-tracked.txt→gate deny(not created); git-ignored.handover/probe.txt→
gate allow_gitignored(created)."근데 이 함수 왜 이렇게 짰어?" and "변수명 더 명확하게 바꾸는 게 낫지 않아?" → no
re-lock; "여기 테스트도 추가해줘" → arm; "그건 됐고, 이제 다른 결제 모듈 논의하자"
→ re-lock. End-to-end: approved=True stays True through a question, flips False on
the unrelated task.
python3 -c "import ast; ast.parse(...)"passes; no staleopens_new/exempt_commandsreferences remain.