Skip to content

guard: stop over-gating non-task writes and over-judging control-command turns #172

Description

@studykit

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):

  1. 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.

  2. 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.

  3. 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.

  4. 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.txtgate 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    taskWorkflow implementation task

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions