Conversation
…ignal Answer "does the harness log refuse-before-dispatch as its own event?" — yes. When a Claude Code user declines a tool's permission prompt (the tool never runs), the receipt's Actions dimension now shows a distinct refused_action_count and an "N actions refused — user denied" line, read from the host's own transcript with transcript_scan provenance. Pipeline: a new refused_tool_call_observed event type (distinct from tool_activity_observed so the executed reducers ignore it and it never triggers the transcript-scan supersession that would drop hook counts) -> anchored transcript matcher (user role + known tool_use id + is_error + host canonical decline prefix) -> per-session observation carrier -> scoped-replace emit -> work-ledger overlay -> task projection -> Actions dimension. Additive only: never folds into or subtracts from executed tool counts, and never touches the evidence/outcome tiers. Forgery-guarded like worksets/finding dispositions: only the transcript-scan emit path stamps the reserved contract key; a generic record_event caller is stripped, so an agent cannot mint a "user denied" claim. A client with no clean per-call refusal signal records nothing, never a fabricated zero. Honest limit: the transcript matcher is best-effort (tool_result content is tool-authored, so a determined agent could emit the anchor text from a failing tool) and the exact host wording should be reconfirmed against a real interactive-mode transcript. Claude Code only today. Full suite green (3025); new tests/test_refused_tool_calls.py (12) cover the matcher via real discovery, the honesty negatives, forgery-is-stripped, and the trusted-path round-trip.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Answers the community question "does your harness log refuse-before-dispatch as its own event, or treat it as if the tool never ran?" — now its own event.
When a Claude Code user declines a tool's permission prompt (refuse-before-dispatch — the tool never runs), the receipt's Actions dimension shows a distinct
refused_action_countand an "N actions refused — user denied" gap line, read from the host's own transcript withtranscript_scanprovenance.Pipeline
client_usage.pytranscript matcher → per-session observation carrier →cli.pyscoped-replace emit →work_ledger.pyoverlay →task_projection.pysum →receipt.py_actions_dimension. A newrefused_tool_call_observedevent type (deliberately nottool_activity_observed, so the executed-count reducers ignore it and it never triggers the transcript-scan supersession that would drop a Claude session's hook counts).Honesty guarantees
0.tool_useid +is_error+ the host's canonical decline prefix; an ordinary tool error (is_erroralone) or a stray id never counts.record_event/POST /eventscaller is stripped (strip_refused_tool_call_provenance), exactly like worksets and finding dispositions.Adversarial review — two findings, both fixed
A 3-lens review of the first draft found two real forgery holes; both are addressed with tests that encode the exact attack:
agentacct_record_eventwithevent_type = refused_tool_call_observedand a chosenrefused_action_countwould forge a "user denied" claim. Fixed: reserved contract key + strip on the generic write lane; the reducer requires the stamp. Test:test_forged_refused_via_record_event_is_stripped_not_trusted.tool_result's content is tool-authored, so a determined agent could emit the anchor text from a failing tool; the original docstring wrongly claimed the text "cannot be injected." Fixed: the docstring now states this is a best-effort capture, not a trust boundary (it can only inflate an additive count, never touch evidence). The exact host wording/structure should be reconfirmed against a real interactive-mode transcript.Known limits (please weigh in)
refused_action_countfield is on the /v1 wire; the human disclosure is the Actions gap line, rendered on every surface. The Swift decoder tolerates additive keys, so no golden impact is expected.Scope / testing
tests/test_refused_tool_calls.py(12 tests: builder/reducer, transcript matcher via real discovery, honesty negatives, forgery-is-stripped, trusted-path round-trip). Full suite green: 3025 passed.