Skip to content

Commit 2c7ad3f

Browse files
committed
Add cross-platform hook wrapper system with Gemini CLI support
This commit introduces a comprehensive hook wrapper system that allows writing hooks once in Python and running them on both Claude Code and Gemini CLI platforms. Changes: Documentation (doc/platform/): - Add README explaining platform documentation purpose - Add Claude Code hooks reference with input/output schemas - Add Gemini CLI hooks reference with input/output schemas - Add learnings files for capturing platform-specific behaviors Hook Wrapper System (src/deepwork/hooks/): - Add wrapper.py with cross-platform input/output normalization - Add claude_hook.sh shell wrapper for Claude Code - Add gemini_hook.sh shell wrapper for Gemini CLI - Add policy_check.py as cross-platform policy evaluation hook - Add README with usage documentation - Update __init__.py with exports and documentation Testing: - Add test_hook_wrapper.py with 35 unit tests for normalization - Add test_hook_wrappers.py with 11 integration tests for shell scripts - All 304 tests pass The wrapper system normalizes: - Event names (Stop/AfterAgent -> after_agent) - Tool names (Write/write_file -> write_file) - Decision values (block -> deny for Gemini) - JSON structure differences between platforms
1 parent 29df4d9 commit 2c7ad3f

13 files changed

Lines changed: 2832 additions & 1 deletion

File tree

doc/platform/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Platform Documentation
2+
3+
This directory contains internal documentation about how different AI CLI platforms behave in ways that matter for DeepWork's hook system and adapter implementations.
4+
5+
## Purpose
6+
7+
These documents capture:
8+
9+
1. **Hook System Behavior** - Input/output formats, blocking mechanisms, event types
10+
2. **Environment Variables** - What each platform provides to hook scripts
11+
3. **Quirks and Edge Cases** - Platform-specific behaviors discovered during development
12+
4. **Learnings** - Insights gained from implementing and testing adapters
13+
14+
## Adding Learnings
15+
16+
**IMPORTANT**: As you work on platform-specific code, document learnings here!
17+
18+
When you discover something about how a platform behaves that isn't obvious from official documentation, add it to the relevant platform's folder. Examples:
19+
20+
- "Gemini CLI's AfterAgent hook doesn't receive transcript_path when the session was resumed"
21+
- "Claude Code's Stop hook JSON must have `decision: block` exactly, not `deny`"
22+
- "Exit code 2 blocks in both platforms but stderr handling differs"
23+
24+
## Directory Structure
25+
26+
```
27+
doc/platform/
28+
├── README.md # This file
29+
├── claude/
30+
│ ├── hooks.md # Claude Code hooks system documentation
31+
│ └── learnings.md # Discovered behaviors and quirks
32+
└── gemini/
33+
├── hooks.md # Gemini CLI hooks system documentation
34+
└── learnings.md # Discovered behaviors and quirks
35+
```
36+
37+
## Platform Comparison Summary
38+
39+
| Feature | Claude Code | Gemini CLI |
40+
|---------|-------------|------------|
41+
| Event: After agent | `Stop` | `AfterAgent` |
42+
| Event: Before tool | `PreToolUse` | `BeforeTool` |
43+
| Event: Before prompt | `UserPromptSubmit` | `BeforeAgent` |
44+
| Project dir env var | `CLAUDE_PROJECT_DIR` | `GEMINI_PROJECT_DIR` |
45+
| Block exit code | `2` | `2` |
46+
| Block decision | `"block"` | `"deny"` or `"block"` |
47+
| Input format | JSON via stdin | JSON via stdin |
48+
| Output format | JSON via stdout | JSON via stdout |
49+
50+
## Related Files
51+
52+
- `src/deepwork/core/adapters.py` - Platform adapter implementations
53+
- `src/deepwork/hooks/` - Hook wrapper scripts
54+
- `doc/platforms/` - External platform documentation (configuration, commands)

doc/platform/claude/hooks.md

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
<!--
2+
Last Updated: 2026-01-15
3+
Source: https://code.claude.com/docs/en/hooks
4+
-->
5+
6+
# Claude Code Hooks System
7+
8+
## Overview
9+
10+
Claude Code hooks are scripts that execute at specific points in Claude's workflow. They enable intercepting and controlling tool execution, validating user input, and performing custom actions.
11+
12+
## Configuration
13+
14+
Hooks are configured in JSON settings files with this precedence (lowest to highest):
15+
16+
1. `~/.claude/settings.json` - User settings
17+
2. `.claude/settings.json` - Project settings
18+
3. `.claude/settings.local.json` - Local project settings (not committed)
19+
4. Managed policy settings
20+
21+
### Configuration Format
22+
23+
```json
24+
{
25+
"hooks": {
26+
"EventName": [
27+
{
28+
"matcher": "ToolPattern",
29+
"hooks": [
30+
{
31+
"type": "command",
32+
"command": "your-command-here",
33+
"timeout": 60
34+
}
35+
]
36+
}
37+
]
38+
}
39+
}
40+
```
41+
42+
### Configuration Fields
43+
44+
| Field | Required | Description |
45+
|-------|----------|-------------|
46+
| `matcher` | For tool events | Pattern to match tool names (regex, `*` wildcard) |
47+
| `type` | Yes | `"command"` for bash or `"prompt"` for LLM evaluation |
48+
| `command` | For type=command | Bash command to execute |
49+
| `prompt` | For type=prompt | LLM prompt to evaluate |
50+
| `timeout` | No | Timeout in seconds (default: 60) |
51+
52+
## Hook Events
53+
54+
### Tool-Related Events (require matcher)
55+
56+
| Event | Description | Timing |
57+
|-------|-------------|--------|
58+
| `PreToolUse` | Before tool execution | Can block or modify |
59+
| `PermissionRequest` | When permission dialog shown | Can auto-approve/deny |
60+
| `PostToolUse` | After tool completes | Can add context |
61+
62+
Common matchers: `Bash`, `Write`, `Edit`, `Read`, `WebFetch`, `Task`, `mcp__*`
63+
64+
### Workflow Events (no matcher needed)
65+
66+
| Event | Description |
67+
|-------|-------------|
68+
| `UserPromptSubmit` | When user submits a prompt |
69+
| `Stop` | When main agent finishes responding |
70+
| `SubagentStop` | When a subagent finishes |
71+
| `PreCompact` | Before compact operation |
72+
| `SessionStart` | When session starts/resumes |
73+
| `SessionEnd` | When session ends |
74+
75+
### Notification Events
76+
77+
| Event | Matchers |
78+
|-------|----------|
79+
| `Notification` | `permission_prompt`, `idle_prompt`, `auth_success`, `elicitation_dialog` |
80+
81+
## Input Schema (stdin)
82+
83+
All hooks receive JSON via stdin:
84+
85+
```json
86+
{
87+
"session_id": "abc123",
88+
"transcript_path": "/path/to/transcript.jsonl",
89+
"cwd": "/current/working/directory",
90+
"permission_mode": "default",
91+
"hook_event_name": "Stop",
92+
"tool_name": "ToolName",
93+
"tool_input": { /* tool-specific fields */ },
94+
"tool_use_id": "toolu_..."
95+
}
96+
```
97+
98+
### Common Input Fields
99+
100+
| Field | Type | Description |
101+
|-------|------|-------------|
102+
| `session_id` | string | Current session identifier |
103+
| `transcript_path` | string | Path to session transcript JSONL |
104+
| `cwd` | string | Current working directory |
105+
| `permission_mode` | string | One of: `default`, `plan`, `acceptEdits`, `dontAsk`, `bypassPermissions` |
106+
| `hook_event_name` | string | The event that triggered this hook |
107+
| `tool_name` | string | Name of the tool (for tool events) |
108+
| `tool_input` | object | Tool-specific input parameters |
109+
| `tool_use_id` | string | Unique identifier for this tool use |
110+
111+
### Tool-Specific Input Examples
112+
113+
**Bash Tool:**
114+
```json
115+
{
116+
"tool_name": "Bash",
117+
"tool_input": {
118+
"command": "npm test",
119+
"description": "Run tests",
120+
"timeout": 120000
121+
}
122+
}
123+
```
124+
125+
**Write Tool:**
126+
```json
127+
{
128+
"tool_name": "Write",
129+
"tool_input": {
130+
"file_path": "/path/to/file.txt",
131+
"content": "file content"
132+
}
133+
}
134+
```
135+
136+
**Edit Tool:**
137+
```json
138+
{
139+
"tool_name": "Edit",
140+
"tool_input": {
141+
"file_path": "/path/to/file.txt",
142+
"old_string": "original text",
143+
"new_string": "replacement text",
144+
"replace_all": false
145+
}
146+
}
147+
```
148+
149+
**Read Tool:**
150+
```json
151+
{
152+
"tool_name": "Read",
153+
"tool_input": {
154+
"file_path": "/path/to/file.txt",
155+
"offset": 0,
156+
"limit": 100
157+
}
158+
}
159+
```
160+
161+
## Output Schema (stdout)
162+
163+
### Exit Codes
164+
165+
| Code | Meaning | Behavior |
166+
|------|---------|----------|
167+
| `0` | Success | stdout parsed as JSON |
168+
| `2` | Blocking error | stderr shown as error, operation blocked |
169+
| Other | Warning | stderr logged, operation continues |
170+
171+
### Common Output Fields
172+
173+
```json
174+
{
175+
"continue": true,
176+
"stopReason": "Message shown when continue is false",
177+
"suppressOutput": true,
178+
"systemMessage": "Optional warning message",
179+
"decision": "block",
180+
"reason": "Explanation for blocking",
181+
"hookSpecificOutput": {
182+
"hookEventName": "EventName"
183+
}
184+
}
185+
```
186+
187+
| Field | Type | Description |
188+
|-------|------|-------------|
189+
| `continue` | boolean | `false` terminates agent loop |
190+
| `stopReason` | string | Message when stopping |
191+
| `suppressOutput` | boolean | Hide from transcript |
192+
| `systemMessage` | string | Warning to display |
193+
| `decision` | string | `"block"` to prevent action |
194+
| `reason` | string | Explanation for decision |
195+
196+
### Event-Specific Output
197+
198+
#### Stop / SubagentStop
199+
200+
Block the agent from stopping:
201+
202+
```json
203+
{
204+
"decision": "block",
205+
"reason": "You must complete task X before stopping"
206+
}
207+
```
208+
209+
Allow stopping (default):
210+
```json
211+
{}
212+
```
213+
214+
#### UserPromptSubmit
215+
216+
Block the prompt:
217+
```json
218+
{
219+
"decision": "block",
220+
"reason": "Cannot process this type of request"
221+
}
222+
```
223+
224+
Add context (text output):
225+
```bash
226+
echo "Current time: $(date)"
227+
exit 0
228+
```
229+
230+
#### PreToolUse
231+
232+
```json
233+
{
234+
"hookSpecificOutput": {
235+
"hookEventName": "PreToolUse",
236+
"permissionDecision": "allow",
237+
"permissionDecisionReason": "Auto-approved",
238+
"updatedInput": {
239+
"field_to_modify": "new value"
240+
}
241+
}
242+
}
243+
```
244+
245+
| permissionDecision | Effect |
246+
|--------------------|--------|
247+
| `"allow"` | Bypass permission, execute tool |
248+
| `"deny"` | Block tool execution |
249+
| `"ask"` | Show permission dialog |
250+
251+
#### PermissionRequest
252+
253+
```json
254+
{
255+
"hookSpecificOutput": {
256+
"hookEventName": "PermissionRequest",
257+
"decision": {
258+
"behavior": "allow",
259+
"updatedInput": {},
260+
"message": "Auto-approved",
261+
"interrupt": false
262+
}
263+
}
264+
}
265+
```
266+
267+
#### PostToolUse
268+
269+
```json
270+
{
271+
"decision": "block",
272+
"reason": "Tool output indicates error",
273+
"hookSpecificOutput": {
274+
"hookEventName": "PostToolUse",
275+
"additionalContext": "Additional info for Claude"
276+
}
277+
}
278+
```
279+
280+
#### SessionStart
281+
282+
```json
283+
{
284+
"hookSpecificOutput": {
285+
"hookEventName": "SessionStart",
286+
"additionalContext": "Context to load at session start"
287+
}
288+
}
289+
```
290+
291+
## Environment Variables
292+
293+
| Variable | Availability | Description |
294+
|----------|--------------|-------------|
295+
| `CLAUDE_PROJECT_DIR` | All hooks | Absolute path to project root |
296+
| `CLAUDE_ENV_FILE` | SessionStart only | File path for persisting env vars |
297+
| `CLAUDE_CODE_REMOTE` | All hooks | `"true"` in web environment |
298+
299+
### Persisting Environment Variables
300+
301+
In SessionStart hooks only:
302+
303+
```bash
304+
#!/bin/bash
305+
if [ -n "$CLAUDE_ENV_FILE" ]; then
306+
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
307+
fi
308+
exit 0
309+
```
310+
311+
## DeepWork Event Mapping
312+
313+
| DeepWork Generic | Claude Code |
314+
|------------------|-------------|
315+
| `after_agent` | `Stop` |
316+
| `before_tool` | `PreToolUse` |
317+
| `before_prompt` | `UserPromptSubmit` |
318+
319+
## Key Behaviors
320+
321+
1. **Exit code 2** is the primary blocking mechanism
322+
2. **JSON with `decision: "block"`** also blocks for Stop hooks
323+
3. **stderr** on exit code 2 is shown to the agent
324+
4. **stdout** on exit code 0 is parsed as JSON
325+
5. **Plain text stdout** is added as context for some events
326+
6. **Multiple hooks** matching the same event run in parallel
327+
7. **Timeout** default is 60 seconds per hook

0 commit comments

Comments
 (0)