Description
Summary
Third PR of the agent hooks Phase 1 stack. It turns PR 2's beforeShellExecution-only parser into the complete Cursor adapter: every option in a hooks.json, all 21 event names, a binding with capabilities and failure semantics for each definition, matcher semantics, the stdin payload for every event Warp fires in Phase 1, and output decoding with the exit-code, malformed-output, and empty-output rules. Events Warp does not fire are parsed, reported, and never fired. This is ai-only code with checked-in fixtures; nothing in the loop changes.
events.rs:CURSOR_EVENTS, a static table with one entry per event: its name ashooks.jsonspells it, the Warp boundary it projects onto (or none for the two Tab hooks andworkspaceOpen), whether Warp fires it in Phase 1, what its matcher is tested against, which output fields Cursor documents, the effect channels those feed, and its control effect: a permission decision (carrying the noun Warp's denial reasons use, whetheruser_messageshows only on a deny, and whataskmeans on the event), a prompt halt, or observe only. The malformed-output and empty-output policies follow from the control effect. Everything else in the adapter reads an event's facts from this table and matches on the event only to pick per-event code.declare_bindingsturns parsed definitions intoDeclaredBindings in table order, then array order.config.rs: the whole file shape.versionmust be1;command,type,timeout,matcher,failClosed,loop_limit,prompt, andmodelare read; unknown event names and unknown definition fields are warnings; a prompt hook or a matcher that does not compile skips that one definition;loop_limitis capped at Warp's host cap of 8. Eachmatcheris compiled here, once.sources.rs: what the adapter keeps about each source betweenparseandbindings: the parsed definitions, the document they came from, and their compiled matchers by pattern text.bindingsreuses the parse when it is handed the same document; matching looks regexes up here and compiles only for a binding that was not declared from a parse.matcher.rs: Rustregex, unanchored, case-sensitive,""and"*"match everything. The value matched depends on the event (table below).facets.rs: the shell and MCP views of a tool payload that both matching and encoding need (the pending command, a finished command with its output and exit code, an MCP call with its result).encode/:mod.rshas the common fields every Cursor hook receives, then the event's own fields, for the twelve Phase 1 events.tool_input.rsis a JSON view of everyAIAgentActionType;tool_output.rsis akind-tagged view of everyAIAgentActionResultType, sent as a JSON string.decode.rs: exit 2, the per-event output fields, Claude's nested output shape where Cursor translates it, malformed and empty output, the failure policy, cancellation, and the strict hook policy.fixtures/:full_config.json,invalid_matcher.json,unknown_event.json,prompt_hook.json,claude_shape_output.json, and one directory per Phase 1 event withhooks.json, thestdin.jsonWarp encodes for a fixed envelope, and a documentedstdout.json.fixtures/README.mdnames the research/01 section each comes from.
Amendments to PR 1 (details under Departures): HookFormatAdapter::decode_output takes a DecodePolicy (the strict hook policy setting), DeclaredBinding carries loop_limit, and HookEffects carries unsupported, the effects a hook asked for that its event cannot produce.
Decisions
Four of these change what a hook can permit or block, and are also recorded in questions.md: only version: 1 is accepted; what ask means on each permission hook; where Claude-shaped output is read; and what the strict hook policy changes.
Event table and bindings
-
Merge order across events in one source (§6.3): bindings are declared in the order Cursor's reference lists the events, then array order. So for one shell command a
preToolUsedefinition merges before abeforeShellExecutionone. Cursor does not define their relative order (research/01 §2.3); a fixed order keeps scalar conflicts deterministic. -
Events Warp does not fire yet (§4.1, §4.4): the six Phase 2 events (
sessionEnd,subagentStart,subagentStop,beforeReadFile,afterFileEdit,preCompact) become bindings on their boundary whosematchesalways returns false, and each definition gets a parse warning ("Warp does not fire X yet; this hook will never run").afterFileEditkeeps thetool.resultboundary but is marked as not firing, since the boundary fires and the facet does not. The three events with no Warp moment (beforeTabFileRead,afterTabFileEdit,workspaceOpen) get a warning per definition and no binding, becauseDeclaredBindingneeds a boundary and none exists. -
Where the "unsupported in Phase 1" status lives: on
CursorEventSpec::fires_in_phase_1, with the activation diagnostic produced as a parse warning on the source. The contract suggested a method on the binding; that would have been a second change to a shared type for something the adapter already knows, and the registry copies parse warnings into the snapshot's diagnostics today. -
Capabilities per event (§4.4), from research/01 §2.4: the six permission hooks declare
VerdictandUserNotice(preToolUseaddsReplacementInput);postToolUsedeclaresReplacementResultandModelContext;postToolUseFailureModelContext;sessionStartSessionEnvandModelContext;beforeSubmitPromptHaltTurnandUserNotice;stopandsubagentStopContinuation;preCompactUserNotice; the observers nothing. The permission hooks do not declareModelContext, becauseagent_messageon a non-deny has no documented meaning (see Decoding), sovalidate_capabilitieswarns only forsessionStart,preToolUse,postToolUse, andpostToolUseFailure, which is where Cursor documents context or a rewrite. -
Per-event output rules (§6.4; research/01 §2.2, §2.4, §6.3, §7; research/05 §4). Malformed output and empty output follow from the control effect: Cursor says output that does not fit a permission hook's schema blocks and states no rule elsewhere, and silence is a missing decision only where the hook decides something. "Fail closed" is what
failClosed: true, the strict hook policy, and a cancelled hook produce; on the events where it means nothing,failClosed: truechanges nothing beyond the invocation log line ("Observers report failures and continue", "A failed stop hook does not continue the turn", "A failedsession.startadds no context or environment").Events Malformed output Empty output Fail closed means askuser_messageon an allowClaude-shaped output beforeShellExecution,beforeMCPExecutiondenial, even when the definition fails open a missing decision; follows failCloseda denial honored as Aska notice ("message shown in client") not read preToolUsedenial a missing decision a denial no opinion with a log line; Askunder the strict hook policydropped with a log line (documented as shown only on a deny) read: hookSpecificOutput.permissionDecision,permissionDecisionReason,updatedInputstand in for the flat fields when those are absentsubagentStartdenial a missing decision a denial a denial with a reason saying so, under both policies dropped (deny-only) not read beforeReadFile,beforeTabFileReaddenial a missing decision a denial not in Cursor's schema, so a schema mismatch (a denial) dropped on beforeReadFile(deny-only); not documented onbeforeTabFileReadnot read beforeSubmitPromptfollows failCloseda missing decision; follows failCloseda halted prompt not applicable a notice when continueistruenot read stop,subagentStopfollows failClosedno opinion (the normal "no follow-up" answer) nothing not applicable not applicable read: decision: "block"with areason, flat or nested, is a follow-up; any otherdecisionis ignoredsessionStart,postToolUse,postToolUseFailure,preCompact, and the observers (sessionEnd,afterShellExecution,afterMCPExecution,afterFileEdit,afterAgentResponse,afterAgentThought,afterTabFileEdit,workspaceOpen)follows failClosedno opinion nothing not applicable preCompact: a notice;sessionStart: accepted and ignored, as Cursor documents; elsewhere undocumentednot read -
loop_limit(§4.5, §9.2; research/01 §1.1): read onstopandsubagentStop; absent is Cursor's default of 5;nulland values above 8 become 8 with a warning; on any other event it is ignored with a warning. The value is stored on the binding and passed through inContinuationRequest.loop_limit; enforcement is PR 7. -
Timeouts (§6.4): the default is the boundary's (30 s pre-execution, 60 s otherwise, 5 s for
sessionEnd);timeoutis seconds, fractions allowed, zero and negative rejected. The activation ceiling is PR 5.
Parsing
version(research/01 §1.1): only1is accepted. Missing,0, a string, a fraction, and any other integer are errors with a message naming the problem, so a future schema is not read with today's meanings. Also recorded inquestions.md.- Unknown event names are a warning and their definitions are skipped; unknown fields inside a definition are a warning each;
promptandmodelon a command hook are accepted silently because Cursor documents them. - Prompt hooks are skipped one definition at a time, with a warning, on every event.
- A matcher that does not compile skips only its definition, with a warning naming the source and index (§6.4). Every other invalid field (missing
command, badtimeout, badloop_limit, unknowntype, a wrongly typed field) is still an error for the file, as in PR 2: those are mistakes the author meant to run, while an unsupported handler and a bad regex are things the architecture says to report and skip. - A matcher on an event with no documented target (
sessionStart,sessionEnd,preCompact,workspaceOpen; research/01 §1.4) is dropped with a warning and the definition matches every occurrence. - A relative
commandresolves against the registry's working directory; PR 5 owns resolution.
Matching (§6.4; research/01 §1.4; coverage §4.2)
-
Regex flavor and anchoring: Rust
regex, unanchored search, case-sensitive. Cursor names none of these. -
A source is read once.
parsecompiles each pattern while it validates the definition and keeps the definitions, the document, and the compiled patterns per source (sources.rs).bindingsis handed the document back by the trait; when it is the documentparsekept for that source, the kept definitions are reused and nothing is read or compiled again. A document the adapter did not parse (another adapter instance did, or the registry changed it) is read again and replaces the entry, so a reload does not accumulate patterns. Matching looks patterns up by text and compiles only for a binding that was not declared from a parse (a test building one by hand). The adapter holds this state because the trait givesbindingsonly the document; without it every pattern compiled twice per activation. -
Matched value per event:
Events Matcher is tested against preToolUse,postToolUse,postToolUseFailurethe canonical tool name ( Shell,Read,Write,Delete,Grep,Task,MCP:<tool>, and Warp's own names such asGlob,SearchCodebase,InitProject)beforeShellExecution,afterShellExecutionthe model's full command beforeMCPExecution,afterMCPExecutionthe MCP tool's own name without the MCP:prefix (Cursor lists no target for these; the tool name is what their payload callstool_name)subagentStart,subagentStopthe subagent type, which no Phase 1 payload carries beforeReadFileReadafterFileEditWritebeforeTabFileReadTabReadafterTabFileEditTabWritebeforeSubmitPromptUserPromptSubmitstopStopafterAgentResponseAgentResponseafterAgentThoughtAgentThoughtsessionStart,sessionEnd,preCompact,workspaceOpeneverything -
afterShellExecutionmatches atool.resultonly when aRequestCommandOutputaction finished (CompletedorShellRecovered). A long-running snapshot, a denied or cancelled command, andReadShellCommandOutput,WriteToLongRunningShellCommand, andTransferShellCommandControlToUserresults do not match: the completion funnel reports a long-running command's exit once more on the command's own action, so the command is counted once (facets.rs; §9.2). -
beforeMCPExecutionandafterMCPExecutionfire forCallMCPToolonly.ReadMCPResourceis reachable throughpreToolUseasMCPResource:<name>; coverage §3.10 suggested also firing the MCP events for it, which would need a made-uptool_name. -
A binding never matches an envelope of another boundary, so a
beforeShellExecutionbinding handed atool.resultreturns false even though both are shell facets.
Encoding (§4.2, §4.5; research/01 §2.1, §3)
- Common fields on every event:
conversation_id,generation_id,modelandmodel_id(when known),model_params: [](Warp has no structured parameters; coverage §3.0 suggests an empty array),hook_event_name,cursor_versionandwarp_version(both Warp's version; §4.5),workspace_roots(the detected roots, or the working directory when none),user_email(nullwhen unknown),transcript_path: null. tool_use_id(the action id) is added to every tool event, as Cursor's release notes document.preToolUse.tool_inputfor a shell command carriescommand,cwd, and the model'sis_read_only,is_risky,wait_until_completion,uses_pager, andrationalewhen present (coverage §3.8 lists these as available beyond Cursor's schema).agent_messageis not available at the interception point and is omitted.afterShellExecutioncarriescommand,output,duration(the payload's measured duration, or the gap between the result's timestamps), andexit_codewhen the shell reported one. Cursor documents no exit code field; it is added because the output alone cannot say whether the command succeeded.sandboxis omitted because Warp does not know it here.- MCP events send
tool_inputas a JSON string, as Cursor documents for them, unlikepreToolUse's object.mcp_server_name,url, andcommandare not known in this crate and are omitted;mcp_server_idis sent under its own name.result_jsonis the MCP result as thermcpcrate serializes it, or{"error": "<message>"}for a failed call. - A cancelled MCP call never reaches
afterMCPExecution: PR 1's completion funnel sends a cancelled result totool.failure, so the MCP view the encoder reads carries either the returned result or the error text and has no cancelled case. - A payload value that cannot be serialized to JSON is written as
nulland logged with the field name, so a hook still receives the rest of the payload. postToolUse.tool_outputis a JSON string of akind-tagged object per result variant (snake_case of the variant). File contents and command output are carried in full (§12: payloads are not redacted); binary file contents and screenshot bytes are summarized by size; theRunAgentsharness_auth_secret_nameis not part oftool_input.postToolUseFailure:error_messageis the result'sDisplaytext;failure_typeispermission_deniedfor a denylisted or hook-denied command and a deniedrun_agents,errorotherwise (Cursor'stimeouthas no Warp source);is_interruptis true for a cancellation; the cancellation reason name is added ascancellation_reason.beforeSubmitPrompt.attachments: each entry of the envelope's attachment summary becomes{ "type": "file", "file_path": <entry> }. The summary is empty until PR 7 fills it; rule attachments will need a typed field on the envelope.sessionStart:session_id(the conversation id),is_background_agent,composer_mode: "agent"(coverage §3.1).stop:status(completed,abortedfor a cancellation,error) andloop_count.afterAgentThought:textandduration_mswhen measured.- Events Warp does not fire encode only the common fields; an event handed a payload it is not about adds nothing.
Decoding (§6.4; research/01 §2.2, §2.4, §6.3, §7; research/05 §4)
- Exit 2 denies on the permission hooks and halts the prompt on
beforeSubmitPrompt, reading any messages printed; on every other event it is a plain failure under the failure policy (which, for those events, means nothing happens). permissionis required on every permission hook: a JSON object without it, or with a value other thanallow,deny, orask, is a schema mismatch and denies even when the definition fails open. Wrongly typed documented fields (user_message: 5) are schema mismatches too; extra undocumented keys are not.ask(§4.5; research/01 §7 item 1; research/05 §4) means something different on each permission hook; the per-event table above has the rule for each, and the event table inevents.rscarries it asAskRule. Also recorded inquestions.md.- Messages on an
ask(§6.5, §9.3): an ask becomes a denial on a surface with no one to ask, so its messages are kept the way a denial's are:agent_message(oruser_messagewhen there is no agent message) is a reason the model can read, and auser_messagethat is not already the reason is a notice. The reason is inHookEffects.reasonsfrom the start, so PR 6's degrade step does not need an adapter change. agent_messageon anallow: Cursor documents it as the message sent to the agent when denied, so it is logged and dropped rather than carried as model context.user_messageon anallowfollows the per-event table above (research/01 §2.4); the event table carries the deny-only flag.updated_input(§4.4, §6.2): on every permission hook it becomesreplacement_input, which PR 1's permission policy denies in Phase 1. Cursor documents the field only onpreToolUse; onbeforeShellExecutionand the others a rewrite request is still a control effect that cannot be dropped silently, so the action is denied rather than run with its original input, and the request is also reported as an unsupported effect naming the event. Claude's nestedupdatedInputcounts only onpreToolUse, where Cursor reads it.additional_contextbecomes model context onsessionStart,postToolUse, andpostToolUseFailure, whichvalidate_capabilitiesalready reports as unsupported in Phase 1; elsewhere it is ignored with a log line.sessionStart.envmust be an object of strings; anything else is a schema mismatch, which forsessionStartmeans no environment and no context.continueanduser_messageare accepted and ignored, as Cursor documents.beforeSubmitPrompt:continueis required;falsehalts withuser_messageas the reason (or a Warp-written one);truemakesuser_messagea notice.stopandsubagentStop: a non-emptyfollowup_messagebecomes a continuation carrying the binding'sloop_limit; an empty one is no follow-up.- Claude-shaped output (research/01 §6.3): accepted exactly where Cursor says it translates it,
preToolUse,stop, andsubagentStop, as the per-event table above shows. On every other eventhookSpecificOutputis an undocumented key, so abeforeShellExecutionhook that answers only in Claude's shape has no decision and denies. Also recorded inquestions.md. - Fields an event does not document (§4.3, §4.4): one that names a control effect Cursor defines on some other event (
permission,hookSpecificOutput,updated_input,updated_mcp_tool_output,continue,followup_message,decision) is never applied and is reported as anUnsupportedEffecton the binding, which the dispatcher already turns into a diagnostic for the source. An observational field the event does not document (additional_context,env, a message) is only logged. A field Cursor documents as accepted and ignored (continueanduser_messageonsessionStart) is neither. - Decision words are compared as written:
permission,decision, andpermissionDecisionmust equalallow,deny,ask, orblockexactly, with no trimming or control-character cleaning, so a value that only cleans up to a known word is a schema mismatch. Messages are still cleaned. - Warp-written reasons name the hook by its configured command after the same cleaning hook messages get, so a command string that carries terminal escapes cannot put them into a reason; a command with no printable text is named by its binding id.
- Strict hook policy (§6.4): for events that can block (the permission hooks and
beforeSubmitPrompt), every evaluation failure, including timeout, crash, empty output, and malformed output, blocks, andpreToolUseaskis honored. It never weakenssubagentStart'sask-means-deny. For observers it changes nothing. The dispatcher passes the default policy until PR 6 wires the setting. Also recorded inquestions.md. - A cancelled hook blocks where it could have blocked (deny or halt) regardless of
failClosed, as PR 2 decided; on observers it does nothing. - Reason and notice text is trimmed and stripped of control characters other than newline and tab. The Warp-written reason strings for the shell hook are the ones PR 2's slice tests pin.
Amendments
DecodePolicy(§6.4): whether a schema-acceptedaskis honored, and whether a failed hook blocks, depends on the vendor event, which only the adapter knows, so the strict setting has to reachdecode_output. It is a per-call input rather than a field on the binding because it is a Warp setting, not a property of the source.DeclaredBinding.loop_limit: the contract asks for the per-definition cap to be stored on the binding so PR 7 can enforce it without reading vendor configuration. Putting it on the binding also lets the decoder pass it through with the continuation.HookEffects.unsupported(§4.4): the architecture's report channel for a control effect a binding cannot produce is the messenger, and the dispatcher already forwardsMergedEffects.unsupportedto it. Only the adapter-to-dispatcher hop was missing;reduce_effectsnow carries each binding's entries into the merged list.
Departures from architecture.md
- Amendment to PR 3 from PR 6 (
65a41d610):report_undocumented_fieldsincrates/ai/src/hooks/adapters/cursor/decode.rsreads a control field's value before reporting it on an event that does not define it. A value that asks for nothing (continue: true,permission: "allow", adecisionother than"block", an emptyfollowup_message) is logged and not reported; a rewrite, a result replacement, and Claude's nested object are still reported. PR 6's dispatcher denies an action before an operation for any reported control field, so without this a benigncontinue: truecopied from Claude-shaped output denied a shell command. Test:undocumented_control_fields_whose_values_ask_for_nothing_are_only_logged. - Amendment to PR 3 from PR 7 (
a779ec806):encode/mod.rs,encode/mod_test.rs, andmatcher_tests.rscarry theprovenancefield PR 7 added toHookPayload::PromptSubmit(PR 1 commit35faca3a5).facets::finished_shell_commandincrates/ai/src/hooks/adapters/cursor/facets.rsreturnsNonefor aReadShellCommandOutputresult, soafterShellExecutionno longer matches a poll of a running command: PR 7's completion funnel reports a long-running command's real exit as a secondtool.resulton the command's ownRequestCommandOutputaction, carrying the command text, output, exit code, and duration, so the event fires exactly once per agent-started command (including ones that finish under a write or a handoff) and not for a user-started command the agent only polls. A matcher test covers aShellReadpoll; theafterMcpExecutionfixture comment is updated because PR 7 also routes an MCP error result totool.result. - Amendment to PR 1 (
79fbe1a2e, formatting6fc1bab54):HookFormatAdapter::decode_outputincrates/ai/src/hooks/adapters/mod.rsgained apolicy: &DecodePolicyparameter, andDeclaredBindinggainedloop_limit: Option<u32>. The dispatcher passesDecodePolicy::default();RecordingAdapterrecords the policies it is given. PR 2's branch got the matching one-line signature and field change in its ownadapters/cursor/files (aa6c0d0a4, formatting8640b2e15) so every branch in the stack builds. - Amendment to PR 1 (
c41714aaa, from review round 1):HookEffectsincrates/ai/src/hooks/model.rsgainedunsupported: Vec<UnsupportedEffect>, andreduce_effectsextends the merged list with each binding's entries, so an adapter can report a control effect its event cannot produce and the dispatcher's existing diagnostic path delivers it.RecordingAdapterfills the field with an empty list. - Follow-through for PR 4's amendment to PR 1 (
32d7f2b21):HookProcessOutcomegainedstdout_truncated(PR 1 commit17c9e32b1), anddecode_outputinadapters/cursor/decode.rstreats an exit 0 with a cut-off stdout as malformed output, even when the kept prefix parses: a denial on the permission hooks and the failure policy elsewhere. The test helpers set the field, anddecode_tests.rscovers every Phase 1 event. - §4.2 says common fields are absent rather than fabricated, and
generation_idmaps to the turn id. Until PR 7 allocates turn ids, every event in a conversation carries the conversation id undergeneration_id, andsessionStartalways will. The value is a real identifier that cannot collide with a turn id, and it is supplied because Cursor listsgeneration_idin the common base of every event, so an absent key could crash scripts that index it; PR 2's slice test asserts the key is present. A script that keys once-per-generation state on it runs once per conversation until PR 7. - §4.1 maps
afterShellExecutionandafterMCPExecutiontotool.result, and PR 1's completion funnel sends any result whereis_failed() || is_cancelled()totool.failure. In this PR those two events therefore do not fire for: a shell command that finished with a non-zero exit code (including exit 130), aShellRecoveredresult, and an MCP tool that returnedCallMCPToolResult::Error(theafterMCPExecution/stdin.jsonfixture is built from that shape on atool.resultpayload, which production cannot produce today). An audit hook on either event sees successes and no failures until PR 7. The matcher and encoder readtool.failurepayloads the same way astool.resultones, so PR 7, which owns the funnel and the result boundaries, can reroute without adapter changes; the recommendation is to classify "the tool ran and returned" astool.resultregardless of the result's success (shellCompletedat any exit code,ShellRecovered,CallMCPToolResult::Error). Recorded inquestions.mdfor PR 7's kickoff. - §4.2 says the MCP facet adds stable server identity and transport metadata. The envelope carries only the server id, so
mcp_server_name,url, andcommandare absent from the MCP payloads. Filling them needs an envelope field thatappresolves from the MCP manager (PR 7 or a later amendment). - Files outside this PR's ownership that changed: none on this branch. The PR 1 amendment and the PR 2 follow-through are on their own branches.
Linked Issue
No linked issue. Part of the agent hooks Phase 1 stack; design is in specs/agent-hooks/architecture.md.
- The linked issue is labeled
ready-to-specorready-to-implement. - Where appropriate, screenshots or a short video of the implementation are included below (especially for user-visible or UI changes).
Testing
All commands run with CARGO_TARGET_DIR=/Users/david/src/warp/target. The warp command also set WARP_LOCAL_SKIP_METAL=1, which this machine needs because it has no Metal toolchain.
cargo check -p ai— passedcargo nextest run -p ai -E 'test(cursor)' --no-fail-fast— passed (95 tests)cargo nextest run -p ai --features local_fs,test-util -E 'test(hooks)' --no-fail-fast— 158 of 160 on this branch; the two others are PR 4's unix process-tree tests, which pass when run alone (the suite includes PR 2's slice tests, which pin the shell reason strings)cargo clippy -p ai --features local_fs,test-util --all-targets --tests -- -D warnings— passedcargo clippy -p ai --all-targets --tests -- -D warnings— passed- On the top of the restacked stack (
david/hooks/07-remaining-boundaries):cargo check -p ai— passed;cargo nextest run -p ai --features local_fs,test-util -E 'test(hooks)'— 265 of 270, the rest being PR 4's process-tree tests (pass alone) and PR 5's real-watcher test (fails alone on this machine before and after this round's commit);WARP_LOCAL_SKIP_METAL=1 cargo nextest run -p warp -E 'test(hooks)'— passed (116 tests) ./script/format— ran last on this branch; a no-op on the top branch- On PR 1's branch after each amendment:
cargo check -p ai --features local_fs,test-utilandcargo nextest run -p ai --features local_fs,test-util -E 'test(hooks)'— passed (35, then 36 tests), plus clippy and the format check for the second. On PR 2's branch after the follow-through: the same two commands — passed (95 tests).
New tests, by module:
-
events: the 21 names in Cursor's order, each resolving back to its entry, with the table length checked against the enum's variant count and no event listed twice; the boundary mapping; the twelve Phase 1 events and the nine that never match; the six permission hooks as the only ones that deny on malformed output; empty-output rules; each event's control effect agreeing with its declared effects; theaskand deny-onlyuser_messagerules of each permission hook; matcher targets; capabilities with and withoutfailClosed; default timeouts;full_config.jsondeclaring 18 bindings in table order with the right options and loop limits; array positions preserved. -
config: a minimal file;full_config.jsonparsing every event and option with its eleven expected warnings; version rules; nohooksobject; invalid JSON;unknown_event.json; unknown fields versus prompt fields;prompt_hook.jsonkeeping positions; unknowntype; missing command; wrongly typed fields; bad and fractional timeouts;invalid_matcher.jsonskipping one definition; match-all matchers; a matcher on an event without a target;loop_limitdefault, zero, in range, capped,null, invalid, and on the wrong event; non-array event values; never-fired events warned per definition; table order regardless of file order. -
matcher: empty,*, and missing matchers; unanchored case-sensitive regex;preToolUseagainstShell,Read,Write,Grep,Task,MCP:read_file,Glob,SearchCodebase, andInitProject; post-tool events on the tool name; shell events on the command text and only for finished commands; MCP events on the bare tool name; literal events; events without a target; boundary mismatches;afterFileEditnever matching on its boundary;Grepas a tool name rather than a command. -
sources: a kept parse answered only for the document it came from, with the regex compiled at parse being the one matching uses; a reparse replacing the source's entry; a pattern that was not kept compiling on first use. -
encode: every Phase 1 event against itsstdin.jsonfixture (JSON carried as text compared as parsed JSON);generation_idfrom the turn id and its fallback to the conversation id; absent and null common fields; both version fields; duration from timestamps and the missing exit code of a recovered shell; the MCP result string; a cancelled tool as an interrupt;tool_inputfor read, edit, MCP, andrun_agentsactions;tool_outputwith text and binary files; never-fired events encoding only common fields; a payload the event is not about. -
decode: for every Phase 1 event, exit 2, five failure kinds under both policies, cancellation, empty output under both policies, non-JSON and schema mismatches under both policies;allow,deny, and their messages on each permission event, including the deny-onlyuser_messageonpreToolUse;askon each event it means something different on, with its messages kept as reasons;updated_inputcarried on every permission event and reported where undocumented; undocumented control fields reported andcontinue: falseonstopunder both policies; decision words compared as written; a cleaned command in Warp-written reasons;additional_context,env,updated_mcp_tool_output;continuefalse and true;followup_messagewith the binding's loop limit; the Claude shapes fromclaude_shape_output.jsonand their rejection elsewhere; strict mode on blocking and non-blocking events; control characters; everystdout.jsonfixture decoding to its expected effects. -
mod: the registry entry; parse warnings as diagnostics on the source; bindings through the trait with activation warnings only where context or rewrites are documented;invalid_matcher.jsonthrough the trait; a shell hook end to end through all five trait methods; a binding under an unknown event name; an undocumentedpermissionfromafterShellExecutionreaching the messenger as a diagnostic through the real dispatcher; a document that stopped parsing;bindingsreusing whatparsekept on the same adapter while a fresh adapter reads the document again and declares the same bindings. -
I have manually tested my changes locally with
./script/run
Manual testing is deferred to the PR 9 instructions; the flag is off and no source activates in production yet.
Agent Mode
- Warp Agent Mode - This PR was created via Warp's AI Agent Mode
CHANGELOG-NONE
Activity
10This pull request is a draft
Merging is unavailable until it is marked ready for review.
masterThis stack of pull requests is managed by Graphite. Learn more about stacking.