Skip to content

fix(cli): preserve caller context so responses use the agent's wire format - #197

Open
tupe12334 wants to merge 1 commit into
mainfrom
fix/issue-196-preserve-caller-response-format
Open

fix(cli): preserve caller context so responses use the agent's wire format#197
tupe12334 wants to merge 1 commit into
mainfrom
fix/issue-196-preserve-caller-response-format

Conversation

@tupe12334

Copy link
Copy Markdown
Member

Fixes #196.

Problem

run_app buffered stdin and called polyhook::parse::parse_event directly. polyhook records the detected caller and event type in thread-locals during read_from, and polyhook::respond reads them back to pick the wire format. Parsing the bytes directly skipped that, leaving caller = Unknown, event = None, so serialize_response_with_event always fell through to serialize_claude_code(resp, None).

Two consequences:

  1. Claude Code PreToolUse blocks terminated the whole session. The legacy top-level decision: "block" aborts the agent session, rather than hookSpecificOutput.permissionDecision: "deny" which denies just that tool call. That also made steplock's own "Then retry your original command" instruction impossible to follow.
  2. Every non-Claude-Code agent got an unparseable response, so the gate failed open — defeating the cross-agent premise in the README.

Change

core/src/bin/main.rs — read via polyhook::read_from(&mut reader) instead of read_to_end + parse::parse_event. The parse import moves into the test module, where it is still used.

Verification

Built from this branch and replayed the same three payloads from the issue:

Caller Before After
Claude Code PreToolUse {"decision":"block",...} {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"..."}}
Cline beforeToolUse {"decision":"block",...} {"approved":false,"reason":"..."}
Windsurf pre_tool {"decision":"block",...} {"allow":false,"reason":"..."}

The approve path is unchanged in practice — polyhook 0.1.11 emits {} for an approve with or without event context — so this PR changes only block responses.

Full end-to-end gate behavior re-verified after the change and still correct: blocks on the current item, repeats the same item until acknowledged, advances only on ack.sh with state persisted to state.json, approves once the checklist reaches [*], resets per session, and branching flows present both options and follow whichever is acknowledged (including rejecting an ambiguous or invalid ack.sh target).

Tests

The existing run_app tests asserted only on the HookResponse enum variant and never serialized, which is why the entire caller-format layer was untested. Added two tests that assert on the serialized JSON:

  • block_uses_claude_code_pre_tool_use_deny_not_session_block
  • block_is_serialized_in_the_calling_agents_format (Cline)

Both fail on main with the exact defect visible in the failure output, and pass with the fix.

Checks run locally

  • cargo test --all-features — 148 passed, 0 failed
  • cargo clippy --all-targets --all-features — clean
  • cargo fmt --check — clean

Opened automatically by the moadim routine "Nightly lib dogfood → issue + PR (polyhook, block-no-verify, steplock)".

…ormat

`run_app` read stdin into a buffer and called `polyhook::parse::parse_event`
directly. polyhook records the detected caller and event type in thread-locals
during `read_from`, and `polyhook::respond` reads them back to choose the wire
format — so parsing the bytes directly left that context at its default
(`caller = Unknown`, `event = None`) and every response was serialized in the
legacy Claude Code shape.

Two consequences: Claude Code `PreToolUse` blocks used the top-level
`decision: "block"` field, which terminates the entire agent session instead of
denying the single tool call, and non-Claude-Code agents (Cline, Windsurf, Amp,
Gemini CLI, ...) received a response they cannot parse, so the gate failed open.

Read through `polyhook::read_from` instead. The approve path is unchanged in
practice — polyhook 0.1.11 emits `{}` for an approve either way.

The existing `run_app` tests only asserted on the `HookResponse` enum variant
and never serialized, leaving the caller-format layer untested; add two tests
that assert on the serialized JSON for Claude Code and Cline.

Fixes #196

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

Hook responses are always serialized in legacy Claude Code format — caller context discarded by parsing stdin instead of reading it

1 participant