Skip to content

pre-tool-use-edit hook blocks Edits despite "Proceeding anyway" message (exit 2 vs exit 0) #19

@lx-0

Description

@lx-0

Summary

The hooks/pre-tool-use-edit hook is documented (in code comments and its own user-facing message) as advisory — but it uses exit 2, which Claude Code's hook contract treats as a block. Users see "Proceeding anyway -- the edit will happen" in the warning, then discover the edit silently didn't land.

This bites every summarize-task invocation because the framework meta-files it must touch (STATE.md, M###-S##-PLAN.md, M###-S##-T##-SUMMARY.md) are never listed in the task-plan's Files section by design, so the scope-drift gate always fires.

Repro

In any ytstack project with an active task (active_task: T01 in STATE.md):

  1. Run /ytstack:summarize-task.
  2. The skill tries to Write M###-S##-T##-SUMMARY.md and Edit M###-S##-PLAN.md + STATE.md.
  3. Each call triggers the hook with Drift warning + Proceeding anyway -- the edit will happen.
  4. Verify on disk: the SUMMARY file does not exist, the checkbox is still [ ], completed_tasks is unchanged, STATE.md status line is unchanged.

Observed on ytstack 0.1.4 via plugin cache at ~/.claude/plugins/cache/yesterday-public-plugins/ytstack/0.1.4/.

Root cause

hooks/pre-tool-use-edit:75-78 (the header comment) claims:

Both gates emit exit-2 warnings, not blocks. […] User-driven session with explicit scope creep is still valid work; the goal is to make it conscious.

…but Claude Code's PreToolUse hook contract treats exit 2 as a deny. So the "advisory" framing is contradicted by the actual return code. Concretely, hooks/pre-tool-use-edit:122:

echo "  Proceeding anyway -- the edit will happen."
} >&2
exit 2

Either the message is wrong or the exit code is wrong.

Proposed fix

Two reasonable options, in order of preference:

Option A — make it truly advisory (matches stated intent): change exit 2 to exit 0 on the drift-warning path. The warning still surfaces via stderr; the tool call proceeds. This matches the in-skill flows (summarize-task, plan-task updating STATE.md, etc.) that legitimately touch meta-files outside the task scope.

Option B — whitelist framework meta-files: before the scope-drift check, exempt these paths so they never trigger:

case "$REL_PATH" in
  .ytstack/STATE.md|.ytstack/M*-PLAN.md|.ytstack/M*-SUMMARY.md|.ytstack/M*-ROADMAP.md)
    exit 0
    ;;
esac

Option A is simpler and preserves the "make scope drift visible" goal without the silent-failure footgun. Option B narrows the exemption but keeps the gate live for genuine source edits. Either fixes the immediate problem.

Workaround

Until fixed, in summarize-task flows, set active_task: none in STATE.md via a sed call (Bash bypasses the hook) before calling Edit/Write on STATE.md / S##-PLAN.md / T##-SUMMARY.md. With active_task=none, the scope-drift gate short-circuits and edits land normally.

Context

Discovered on SunoFlow (M001-S01-T01) while running /ytstack:summarize-task. Three consecutive Edit/Write calls were silently dropped despite the "proceeding anyway" message. Catalogue artifact (the actual T01 deliverable) had landed fine because it was listed in the task plan's Files section as Create; the meta-file updates that follow are what broke.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions