Skip to content

feat(annotate): replace --silent-approve with --hook#610

Merged
backnotprop merged 4 commits intomainfrom
feat/hook-flag
Apr 24, 2026
Merged

feat(annotate): replace --silent-approve with --hook#610
backnotprop merged 4 commits intomainfrom
feat/hook-flag

Conversation

@backnotprop
Copy link
Copy Markdown
Owner

Summary

  • Replaces --silent-approve with --hook, which emits hook-native JSON that works directly with Claude Code and Codex PostToolUse/Stop hook protocols
  • --hook implies --gate (always three-button UX) and supersedes --json if both are passed
  • No wrapper script needed: plannotator annotate "$FILE" --hook is a drop-in hook command

Why

After shipping #606, we discovered that --silent-approve solves a problem that doesn't exist:

  • Claude Code PostToolUse: plain text stdout is added to transcript as context, it does NOT block. Only {"decision":"block","reason":"..."} blocks.
  • Codex PostToolUse: plain text stdout is ignored entirely. Only JSON is processed.
  • Claude Code Stop: plain text stdout goes to debug log, NOT added to context. Only {"decision":"block","reason":"..."} prevents stopping.

Stdout matrix

Flags Approve Close Annotate
--gate The user approved. empty feedback (plaintext)
--json n/a {"decision":"dismissed"} {"decision":"annotated","feedback":"..."}
--gate --json {"decision":"approved"} {"decision":"dismissed"} {"decision":"annotated","feedback":"..."}
--hook empty empty {"decision":"block","reason":"..."}

Hook recipe

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write",
      "hooks": [{
        "type": "command",
        "command": "plannotator annotate \"$CLAUDE_TOOL_INPUT_file_path\" --hook",
        "timeout": 345600
      }]
    }]
  }
}

Follow-up to #606. Refs #570.

Test plan

  • bun test packages/shared/ apps/opencode-plugin/ apps/hook/server/ — 205 pass
  • grep -rn "silent.approve\|silentApprove" apps/ packages/ — zero results
  • Manual: plannotator annotate README.md --hook → 3-button UX → Approve → empty stdout → Annotate → {"decision":"block","reason":"..."} on stdout

…protocol

--silent-approve was designed for "naive hooks that treat any stdout as
a block signal" but neither Claude Code nor Codex hooks work that way.
Plain text stdout doesn't block in either system -- only the JSON
protocol {"decision":"block","reason":"..."} does.

--hook emits hook-native JSON directly:
  Approve/Close → empty stdout (hook passes)
  Annotate → {"decision":"block","reason":"<feedback>"} (hook blocks)

--hook implies --gate (always three-button UX) and supersedes --json
if both are passed. Works with Claude Code PostToolUse, Stop hooks,
and Codex equivalents without a wrapper script.

For provenance purposes, this commit was AI assisted.
Restructures the guide around the --hook workflow, explains env vars
like $CLAUDE_TOOL_INPUT_file_path, and shows how PostToolUse and Stop
hooks combine for full turn-by-turn review.

For provenance purposes, this commit was AI assisted.
Removes Claude Code-specific framing. Documents env var differences
across agents, positions --hook as working with any agent that uses
the {"decision":"block"} protocol.

For provenance purposes, this commit was AI assisted.
Moves the implication into parseAnnotateArgs so every consumer
(OpenCode, Pi) gets gate mode automatically. Previously only the
binary applied it.

For provenance purposes, this commit was AI assisted.
@backnotprop backnotprop merged commit 364cd87 into main Apr 24, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant