forge_primitives/hooks/inputs.rs
1//! Typed input payloads for each hook event.
2//!
3//! Every concrete
4//! input flattens [`BaseHookInput`] so callbacks can access `session_id`,
5//! `transcript_path`, `cwd`, and (optionally) `permission_mode` without
6//! reaching back into [`super::HookContext`].
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11/// Fields shared by every hook input the CLI emits. Every concrete
12/// input type below flattens this struct so callbacks can access
13/// `session_id`, `transcript_path`, `cwd`, and (optionally)
14/// `permission_mode` without reaching back into
15/// [`super::HookContext`].
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct BaseHookInput {
18 /// Session identifier the hook fired in.
19 pub session_id: String,
20 /// Filesystem path of the session transcript JSONL file.
21 pub transcript_path: String,
22 /// Working directory of the CLI when the hook fired.
23 pub cwd: String,
24 /// Permission mode active for the call (e.g. `"default"`,
25 /// `"bypassPermissions"`). Absent for hook frames that predate this
26 /// field or where the CLI omits it.
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub permission_mode: Option<String>,
29 /// Active effort level at the moment the hook fired (CLI 2.1.133+).
30 /// `None` for older CLI versions or hook frames that predate the
31 /// field. The wire shape is `{"effort": {"level": "max"}}` per the
32 /// 2.1.133 changelog ("Hooks now receive the active effort level
33 /// via the `effort.level` JSON input field"). Defensive Option lets
34 /// older baselines decode without the field present.
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub effort: Option<EffortInfo>,
37}
38
39/// Wrapper for the `effort` field on hook inputs. Currently only
40/// carries `level` but kept as a struct so future fields (budget,
41/// adaptive flags, …) can be added without breaking decode-side
42/// pattern matches.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct EffortInfo {
45 /// Effort level string (`"low"` / `"medium"` / `"high"` /
46 /// `"xhigh"` / `"max"`). Kept as `String` rather than a typed
47 /// enum so future levels the CLI introduces don't fail decode;
48 /// consumers map to `crate::EffortLevel` when they need typed
49 /// access.
50 pub level: String,
51}
52
53/// Optional sub-agent attribution present on tool-lifecycle hook
54/// inputs (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`,
55/// `PermissionRequest`). Fields are populated when the hook fires
56/// inside a `Task`-spawned sub-agent so tool events can be
57/// attributed back to the right agent when multiple sub-agents
58/// interleave over the same control channel.
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct SubagentContext {
61 /// Sub-agent identifier. Matches the `agent_id` emitted by that
62 /// sub-agent's `SubagentStart` / `SubagentStop` hooks.
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub agent_id: Option<String>,
65 /// Agent type name (e.g. `"general-purpose"`, `"code-reviewer"`).
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub agent_type: Option<String>,
68}
69
70/// Input payload for `PreToolUse`.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct PreToolUseInput {
73 /// Shared hook context (`session_id`, `transcript_path`, `cwd`, …).
74 #[serde(flatten)]
75 pub base: BaseHookInput,
76 /// Optional sub-agent attribution.
77 #[serde(flatten)]
78 pub subagent: SubagentContext,
79 /// Tool the model wants to invoke.
80 pub tool_name: String,
81 /// The model's proposed input.
82 pub tool_input: Value,
83 /// Opaque tool-use identifier the CLI will reference in the matching
84 /// `PostToolUse` / `PostToolUseFailure` frame.
85 pub tool_use_id: String,
86}
87
88/// Input payload for `PostToolUse`.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct PostToolUseInput {
91 /// Shared hook context.
92 #[serde(flatten)]
93 pub base: BaseHookInput,
94 /// Optional sub-agent attribution.
95 #[serde(flatten)]
96 pub subagent: SubagentContext,
97 /// Tool that was invoked.
98 pub tool_name: String,
99 /// The input the tool actually ran with.
100 pub tool_input: Value,
101 /// The tool's response payload.
102 pub tool_response: Value,
103 /// Tool-use identifier matching the preceding `PreToolUse` frame.
104 pub tool_use_id: String,
105}
106
107/// Input payload for `PostToolUseFailure`.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct PostToolUseFailureInput {
110 /// Shared hook context.
111 #[serde(flatten)]
112 pub base: BaseHookInput,
113 /// Optional sub-agent attribution.
114 #[serde(flatten)]
115 pub subagent: SubagentContext,
116 /// Tool that was invoked and failed.
117 pub tool_name: String,
118 /// Input the tool ran with.
119 pub tool_input: Value,
120 /// Tool-use identifier matching the preceding `PreToolUse` frame.
121 pub tool_use_id: String,
122 /// Error message the tool surfaced.
123 pub error: String,
124 /// `Some(true)` when the failure was caused by user interruption; `None`
125 /// when the CLI omits the field (upstream `NotRequired`).
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub is_interrupt: Option<bool>,
128}
129
130/// Input payload for `UserPromptSubmit`.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct UserPromptSubmitInput {
133 /// Shared hook context.
134 #[serde(flatten)]
135 pub base: BaseHookInput,
136 /// Raw prompt text the user submitted.
137 pub prompt: String,
138}
139
140/// Input payload for `Stop`.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct StopInput {
143 /// Shared hook context.
144 #[serde(flatten)]
145 pub base: BaseHookInput,
146 /// `true` when the stop-hook chain was previously active (re-entrant
147 /// call). Callbacks typically return `HookDecision::passthrough()`
148 /// when this is set to avoid infinite loops.
149 pub stop_hook_active: bool,
150}
151
152/// Input payload for `SubagentStop`.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct SubagentStopInput {
155 /// Shared hook context.
156 #[serde(flatten)]
157 pub base: BaseHookInput,
158 /// Re-entrancy indicator (see [`StopInput::stop_hook_active`]).
159 pub stop_hook_active: bool,
160 /// Sub-agent identifier.
161 pub agent_id: String,
162 /// Filesystem path of the sub-agent's transcript JSONL file.
163 pub agent_transcript_path: String,
164 /// Agent type (e.g. `"general-purpose"`).
165 pub agent_type: String,
166}
167
168/// Input payload for `PreCompact`.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct PreCompactInput {
171 /// Shared hook context.
172 #[serde(flatten)]
173 pub base: BaseHookInput,
174 /// Trigger that prompted the compaction - `"manual"` or `"auto"`.
175 pub trigger: String,
176 /// Caller-supplied compaction guidance. `None` when the CLI did not
177 /// pass custom instructions.
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub custom_instructions: Option<String>,
180}
181
182/// Input payload for `Notification`.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct NotificationInput {
185 /// Shared hook context.
186 #[serde(flatten)]
187 pub base: BaseHookInput,
188 /// Body of the notification.
189 pub message: String,
190 /// Optional title; absent when the CLI omits it.
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub title: Option<String>,
193 /// Notification classification (e.g. `"permission_request"`).
194 pub notification_type: String,
195}
196
197/// Input payload for `SubagentStart`.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct SubagentStartInput {
200 /// Shared hook context.
201 #[serde(flatten)]
202 pub base: BaseHookInput,
203 /// Sub-agent identifier for this spawn.
204 pub agent_id: String,
205 /// Agent type (e.g. `"general-purpose"`).
206 pub agent_type: String,
207}
208
209/// Input payload for `PermissionRequest`.
210///
211/// Observed when the CLI asks the SDK to confirm a tool invocation. This is
212/// distinct from the `can_use_tool` `control_request` path - `PermissionRequest`
213/// hooks are observational; actual allow/deny decisions still flow through
214/// `can_use_tool`.
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct PermissionRequestInput {
217 /// Shared hook context.
218 #[serde(flatten)]
219 pub base: BaseHookInput,
220 /// Optional sub-agent attribution.
221 #[serde(flatten)]
222 pub subagent: SubagentContext,
223 /// Tool the CLI is asking about.
224 pub tool_name: String,
225 /// Proposed tool input.
226 pub tool_input: Value,
227 /// Optional list of permission-rule suggestions the CLI proposes.
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub permission_suggestions: Option<Vec<Value>>,
230}