@globant/coda-darwin-x64 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agents/coda-help.md +166 -0
- package/assets/agents/create-workflow.md +264 -0
- package/assets/agents/explore.md +26 -0
- package/assets/docs/agents.md +162 -0
- package/assets/docs/cli-reference.md +131 -0
- package/assets/docs/cli-vs-batch.md +58 -0
- package/assets/docs/config-json.md +314 -0
- package/assets/docs/config-reference.md +329 -0
- package/assets/docs/configuration.md +105 -0
- package/assets/docs/connect-provider.md +77 -0
- package/assets/docs/extensions.md +260 -0
- package/assets/docs/faq.md +152 -0
- package/assets/docs/glossary.md +41 -0
- package/assets/docs/guide-automate.md +135 -0
- package/assets/docs/guide-changes.md +101 -0
- package/assets/docs/guide-collaborate.md +119 -0
- package/assets/docs/guide-extend.md +120 -0
- package/assets/docs/guide-understand.md +95 -0
- package/assets/docs/hooks.md +704 -0
- package/assets/docs/how-it-works.md +73 -0
- package/assets/docs/index.md +62 -0
- package/assets/docs/installation.md +71 -0
- package/assets/docs/logging.md +123 -0
- package/assets/docs/overview.md +91 -0
- package/assets/docs/permissions.md +93 -0
- package/assets/docs/quickstart.md +104 -0
- package/assets/docs/sessions.md +139 -0
- package/assets/docs/shortcuts.md +61 -0
- package/assets/docs/tools-reference.md +81 -0
- package/assets/docs/workflows.md +146 -0
- package/assets/skills/create-extension/SKILL.md +293 -0
- package/assets/skills/create-hook/SKILL.md +442 -0
- package/assets/skills/create-skill/SKILL.md +180 -0
- package/assets/skills/plan/SKILL.md +25 -0
- package/coda +0 -0
- package/lib/keytar/build/Release/keytar.node +0 -0
- package/lib/keytar/lib/keytar.js +43 -0
- package/lib/opentui/assets/javascript/highlights.scm +205 -0
- package/lib/opentui/assets/javascript/tree-sitter-javascript.wasm +0 -0
- package/lib/opentui/assets/markdown/highlights.scm +150 -0
- package/lib/opentui/assets/markdown/injections.scm +27 -0
- package/lib/opentui/assets/markdown/tree-sitter-markdown.wasm +0 -0
- package/lib/opentui/assets/markdown_inline/highlights.scm +115 -0
- package/lib/opentui/assets/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
- package/lib/opentui/assets/typescript/highlights.scm +604 -0
- package/lib/opentui/assets/typescript/tree-sitter-typescript.wasm +0 -0
- package/lib/opentui/assets/zig/highlights.scm +284 -0
- package/lib/opentui/assets/zig/tree-sitter-zig.wasm +0 -0
- package/lib/opentui/libopentui.dylib +0 -0
- package/lib/opentui/parser.worker.js +4244 -0
- package/lib/opentui/tree-sitter-3jzf13jk.wasm +0 -0
- package/lib/ripgrep/COPYING +3 -0
- package/lib/ripgrep/LICENSE-MIT +21 -0
- package/lib/ripgrep/UNLICENSE +24 -0
- package/lib/ripgrep/rg +0 -0
- package/package.json +20 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
# Lifecycle hooks
|
|
2
|
+
|
|
3
|
+
**Hooks** are user-defined shell commands or HTTP endpoints that run automatically at specific points in a Coda session. They let you enforce policies, inject context, gate dangerous operations, run linters, log activity, and integrate with external systems — without modifying Coda's source code.
|
|
4
|
+
|
|
5
|
+
The hook system is **compatible with Claude Code hooks**: the same JSON input/output contract, the same config format, and the same `hooks/hooks.json` plugin convention all work in Coda.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
Add a `hooks` key to `~/.coda/config.json` (global) or `.coda/config.json` (project):
|
|
12
|
+
|
|
13
|
+
```jsonc
|
|
14
|
+
{
|
|
15
|
+
"hooks": {
|
|
16
|
+
"PreToolUse": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "Bash",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "echo 'About to run a Bash command' >> ~/.coda/hooks.log"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Restart Coda (or run `/reload-hooks`) for the change to take effect.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Where hooks are configured
|
|
36
|
+
|
|
37
|
+
| Source | Location | Priority |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| **Project hooks** | `.coda/config.json` → `hooks` key | Highest — run first |
|
|
40
|
+
| **Global hooks** | `~/.coda/config.json` → `hooks` key | Second |
|
|
41
|
+
| **Plugin hooks** | `hooks/hooks.json` inside an installed plugin | Lowest — run last |
|
|
42
|
+
|
|
43
|
+
When the same hook command appears in more than one source for the same event and matcher, only the highest-priority copy runs (deduplication by `event + matcher + type + command/url`).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Hook events
|
|
48
|
+
|
|
49
|
+
Each event fires at a specific point in the session lifecycle.
|
|
50
|
+
|
|
51
|
+
### Tool events
|
|
52
|
+
|
|
53
|
+
| Event | When it fires | Can block? |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `PreToolUse` | Before a tool executes | **Yes** — return `decision: "block"` or exit code 2 |
|
|
56
|
+
| `PostToolUse` | After a tool succeeds | No — can inject `additionalContext` |
|
|
57
|
+
| `PostToolUseFailure` | After a tool throws an error | No — informational |
|
|
58
|
+
|
|
59
|
+
The **matcher** on tool events is tested against the **tool name**. g-coda's runtime tool names are lower-case — `bash`, `write`, `edit`, `read`, `glob`, `grep` — but exact / pipe-separated matchers are **case-insensitive**, so Claude Code's capitalized names (`"Bash"`) also match. (Regex matchers, e.g. `"^mcp__.*"`, are matched case-sensitively.)
|
|
60
|
+
|
|
61
|
+
### Session events
|
|
62
|
+
|
|
63
|
+
| Event | When it fires | Can block? |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `SessionStart` | When a session is created | No |
|
|
66
|
+
| `SessionEnd` | When a session ends | No |
|
|
67
|
+
| `Stop` | When the agent loop finishes a turn | No |
|
|
68
|
+
| `PreCompact` | Before context compaction runs | **Yes** — return `decision: "block"` to cancel |
|
|
69
|
+
| `PostCompact` | After context compaction completes | No |
|
|
70
|
+
|
|
71
|
+
### Prompt events
|
|
72
|
+
|
|
73
|
+
| Event | When it fires | Can block? |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `UserPromptSubmit` | When the user submits a message | **Yes** — can block or rewrite the prompt |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Configuration schema
|
|
80
|
+
|
|
81
|
+
### Full example
|
|
82
|
+
|
|
83
|
+
```jsonc
|
|
84
|
+
{
|
|
85
|
+
"hooks": {
|
|
86
|
+
"PreToolUse": [
|
|
87
|
+
{
|
|
88
|
+
"matcher": "Bash",
|
|
89
|
+
"hooks": [
|
|
90
|
+
{
|
|
91
|
+
"type": "command",
|
|
92
|
+
"command": ".coda/hooks/validate-bash.sh",
|
|
93
|
+
"timeout": 10
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"matcher": "write|edit",
|
|
99
|
+
"hooks": [
|
|
100
|
+
{
|
|
101
|
+
"type": "command",
|
|
102
|
+
"command": ".coda/hooks/check-file-policy.sh"
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"PostToolUse": [
|
|
108
|
+
{
|
|
109
|
+
"matcher": "write|edit",
|
|
110
|
+
"hooks": [
|
|
111
|
+
{
|
|
112
|
+
"type": "command",
|
|
113
|
+
"command": "npx prettier --write \"$(jq -r .tool_input.file_path)\""
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
"UserPromptSubmit": [
|
|
119
|
+
{
|
|
120
|
+
"hooks": [
|
|
121
|
+
{
|
|
122
|
+
"type": "http",
|
|
123
|
+
"url": "http://localhost:8080/hooks/prompt",
|
|
124
|
+
"timeout": 5
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
"SessionStart": [
|
|
130
|
+
{
|
|
131
|
+
"hooks": [
|
|
132
|
+
{
|
|
133
|
+
"type": "command",
|
|
134
|
+
"command": "echo \"Session started at $(date)\" >> ~/.coda/sessions.log"
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Matcher patterns
|
|
144
|
+
|
|
145
|
+
The `matcher` field filters which tool names (or session sources) trigger the hook. It is optional — omit it or use `"*"` to match everything.
|
|
146
|
+
|
|
147
|
+
| Pattern | Example | Matches |
|
|
148
|
+
| --- | --- | --- |
|
|
149
|
+
| Omitted or `"*"` | — | All values |
|
|
150
|
+
| Exact string (case-insensitive) | `"bash"` / `"Bash"` | The `bash` tool |
|
|
151
|
+
| Pipe-separated list (case-insensitive) | `"write\|edit"` | `write` or `edit` |
|
|
152
|
+
| JavaScript regex (case-sensitive) | `"^mcp__.*"` | Any MCP tool |
|
|
153
|
+
|
|
154
|
+
### Hook handler fields
|
|
155
|
+
|
|
156
|
+
#### Command hook (`type: "command"`)
|
|
157
|
+
|
|
158
|
+
| Field | Required | Description |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `type` | ✅ | `"command"` |
|
|
161
|
+
| `command` | ✅ | The command to run. In **shell form** (no `args`) it is the command string passed to a shell. In **exec form** (`args` present) it is the executable to spawn directly. Receives JSON on stdin. |
|
|
162
|
+
| `args` | — | Argument vector. When present (even `[]`), the hook runs in **exec form**: `command` is spawned directly with `args` as its arguments and **no shell** — each element is passed verbatim (no quoting/globbing). `shell` is ignored. Path placeholders are substituted in each element. |
|
|
163
|
+
| `shell` | — | Shell for **shell form** only (ignored when `args` is set): `"bash"` (→ `bash -c`) or `"powershell"` (→ `powershell -NoProfile -Command`). Defaults to `sh -c`. |
|
|
164
|
+
| `timeout` | — | Seconds before the hook is killed (default: 600). |
|
|
165
|
+
| `if` | — | Permission rule — conditionally skip the hook (see [Conditional execution](#conditional-execution-if-field)). |
|
|
166
|
+
| `statusMessage` | — | Custom spinner label shown while the hook runs. |
|
|
167
|
+
| `async` | — | `true` → run in background; results polled at turn boundaries. |
|
|
168
|
+
| `asyncRewake` | — | `true` → run detached; on exit code 2 the model is re-engaged with the hook's output. |
|
|
169
|
+
| `once` | — | `true` → run once per session then remove (schema accepted; not yet enforced). |
|
|
170
|
+
|
|
171
|
+
#### HTTP hook (`type: "http"`)
|
|
172
|
+
|
|
173
|
+
| Field | Required | Description |
|
|
174
|
+
| --- | --- | --- |
|
|
175
|
+
| `type` | ✅ | `"http"` |
|
|
176
|
+
| `url` | ✅ | URL to POST JSON to. |
|
|
177
|
+
| `headers` | — | Extra request headers. `$VAR` and `${VAR}` references anywhere in a value (e.g. `"Bearer $MY_TOKEN"`) are interpolated from env, but only for names listed in `allowedEnvVars`. References to unlisted variables are replaced with an empty string. |
|
|
178
|
+
| `allowedEnvVars` | — | Env var names whose values may be interpolated into `headers`. |
|
|
179
|
+
| `timeout` | — | Seconds before the request is aborted (default: 600). |
|
|
180
|
+
| `async` | — | `true` → fire-and-forget: the POST is dispatched in the background and does not block the lifecycle event. Async HTTP hooks cannot block or return a decision; the response is logged but not applied. |
|
|
181
|
+
| `if` | — | Permission rule (see [Conditional execution](#conditional-execution-if-field)). |
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## JSON input (what your hook receives)
|
|
186
|
+
|
|
187
|
+
Coda sends a JSON object on **stdin** (command hooks) or as the **POST body** (HTTP hooks). All events include these base fields:
|
|
188
|
+
|
|
189
|
+
```jsonc
|
|
190
|
+
{
|
|
191
|
+
"session_id": "abc123",
|
|
192
|
+
"transcript_path": "",
|
|
193
|
+
"cwd": "/home/user/my-project",
|
|
194
|
+
"hook_event_name": "PreToolUse"
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Tool events (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`)
|
|
199
|
+
|
|
200
|
+
```jsonc
|
|
201
|
+
{
|
|
202
|
+
"session_id": "abc123",
|
|
203
|
+
"transcript_path": "",
|
|
204
|
+
"cwd": "/home/user/my-project",
|
|
205
|
+
"hook_event_name": "PreToolUse",
|
|
206
|
+
"tool_name": "Bash",
|
|
207
|
+
"tool_input": { "command": "git status", "description": "Check git status" },
|
|
208
|
+
"tool_use_id": "toolu_01XYZ",
|
|
209
|
+
// PostToolUse only:
|
|
210
|
+
"tool_response": "On branch main\n...",
|
|
211
|
+
// PostToolUseFailure only:
|
|
212
|
+
"error": "Command not found: gti"
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### `UserPromptSubmit`
|
|
217
|
+
|
|
218
|
+
```jsonc
|
|
219
|
+
{
|
|
220
|
+
"session_id": "abc123",
|
|
221
|
+
"transcript_path": "",
|
|
222
|
+
"cwd": "/home/user/my-project",
|
|
223
|
+
"hook_event_name": "UserPromptSubmit",
|
|
224
|
+
"prompt": "Refactor the auth module"
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### `SessionStart`
|
|
229
|
+
|
|
230
|
+
```jsonc
|
|
231
|
+
{
|
|
232
|
+
"session_id": "abc123",
|
|
233
|
+
"transcript_path": "",
|
|
234
|
+
"cwd": "/home/user/my-project",
|
|
235
|
+
"hook_event_name": "SessionStart",
|
|
236
|
+
"source": "startup"
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### `Stop`
|
|
241
|
+
|
|
242
|
+
```jsonc
|
|
243
|
+
{
|
|
244
|
+
"session_id": "abc123",
|
|
245
|
+
"transcript_path": "",
|
|
246
|
+
"cwd": "/home/user/my-project",
|
|
247
|
+
"hook_event_name": "Stop",
|
|
248
|
+
"stop_reason": "turn_complete"
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## JSON output (what your hook can return)
|
|
255
|
+
|
|
256
|
+
Your hook can print a JSON object to **stdout** (command hooks) or return it as the **response body** (HTTP hooks). All fields are optional.
|
|
257
|
+
|
|
258
|
+
```jsonc
|
|
259
|
+
{
|
|
260
|
+
// Stop the agent entirely (any event)
|
|
261
|
+
"continue": false,
|
|
262
|
+
"stopReason": "Policy violation detected",
|
|
263
|
+
|
|
264
|
+
// Show a warning to the user without stopping
|
|
265
|
+
"systemMessage": "Warning: this file is read-only",
|
|
266
|
+
|
|
267
|
+
// Block the current operation (PreToolUse, UserPromptSubmit, PreCompact)
|
|
268
|
+
"decision": "block",
|
|
269
|
+
"reason": "Bash commands are not allowed in this project",
|
|
270
|
+
|
|
271
|
+
// Event-specific output
|
|
272
|
+
"hookSpecificOutput": {
|
|
273
|
+
"hookEventName": "PreToolUse",
|
|
274
|
+
|
|
275
|
+
// PreToolUse: override the permission decision
|
|
276
|
+
"permissionDecision": "allow", // or "deny" or "ask"
|
|
277
|
+
"permissionDecisionReason": "Approved by policy engine",
|
|
278
|
+
|
|
279
|
+
// PreToolUse: rewrite the tool's input before it runs
|
|
280
|
+
"updatedInput": { "command": "git status --short" },
|
|
281
|
+
|
|
282
|
+
// PostToolUse: append context to the tool result (shown to the model)
|
|
283
|
+
"additionalContext": "Lint passed. No issues found.",
|
|
284
|
+
|
|
285
|
+
// UserPromptSubmit: rewrite the user's prompt
|
|
286
|
+
"updatedPrompt": "Refactor the auth module. Follow the style guide in STYLE.md."
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Exit codes (command hooks only)
|
|
292
|
+
|
|
293
|
+
| Exit code | Meaning |
|
|
294
|
+
| --- | --- |
|
|
295
|
+
| `0` | Success — stdout is parsed for JSON output |
|
|
296
|
+
| `2` | **Blocking error** — stderr (or stdout) is shown to the user/model; the operation is blocked |
|
|
297
|
+
| Any other | Non-blocking error — execution continues; stderr is logged |
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Blocking operations
|
|
302
|
+
|
|
303
|
+
To **block** a tool call or prompt, use either:
|
|
304
|
+
|
|
305
|
+
- **Exit code 2** — the simplest approach; stderr becomes the error message
|
|
306
|
+
- **JSON output** with `"decision": "block"` and an optional `"reason"`
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
#!/bin/bash
|
|
310
|
+
# .coda/hooks/no-rm.sh
|
|
311
|
+
INPUT=$(cat)
|
|
312
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
|
313
|
+
|
|
314
|
+
if echo "$COMMAND" | grep -qE '\brm\b.*-rf'; then
|
|
315
|
+
echo "Blocked: rm -rf is not allowed" >&2
|
|
316
|
+
exit 2
|
|
317
|
+
fi
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
```jsonc
|
|
321
|
+
// config.json
|
|
322
|
+
{
|
|
323
|
+
"hooks": {
|
|
324
|
+
"PreToolUse": [
|
|
325
|
+
{
|
|
326
|
+
"matcher": "Bash",
|
|
327
|
+
"hooks": [{ "type": "command", "command": ".coda/hooks/no-rm.sh" }]
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Injecting context into tool results
|
|
337
|
+
|
|
338
|
+
A `PostToolUse` hook can append information to the tool result that the model sees as part of the response. Use `hookSpecificOutput.additionalContext`:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
#!/bin/bash
|
|
342
|
+
# .coda/hooks/lint-after-write.sh
|
|
343
|
+
INPUT=$(cat)
|
|
344
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
345
|
+
|
|
346
|
+
if [ -n "$FILE" ]; then
|
|
347
|
+
RESULT=$(npx eslint "$FILE" --format compact 2>&1)
|
|
348
|
+
if [ -n "$RESULT" ]; then
|
|
349
|
+
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"ESLint: $RESULT\"}}"
|
|
350
|
+
fi
|
|
351
|
+
fi
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
The model sees the tool result followed by `[Hook] ESLint: ...`.
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Rewriting prompts
|
|
359
|
+
|
|
360
|
+
A `UserPromptSubmit` hook can modify the user's message before the model sees it:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
#!/bin/bash
|
|
364
|
+
INPUT=$(cat)
|
|
365
|
+
PROMPT=$(echo "$INPUT" | jq -r '.prompt')
|
|
366
|
+
ENHANCED="$PROMPT\n\nContext: $(cat STYLE.md 2>/dev/null | head -20)"
|
|
367
|
+
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"UserPromptSubmit\",\"updatedPrompt\":\"$ENHANCED\"}}"
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Conditional execution (`if` field)
|
|
373
|
+
|
|
374
|
+
The `if` field conditionally skips a hook based on the tool name and its input. It uses the same permission rule syntax as Claude Code:
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
rule ::= toolName | toolName(content)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
| Pattern | Example | Matches |
|
|
381
|
+
| --- | --- | --- |
|
|
382
|
+
| Tool name only | `"Bash"` | Any Bash call |
|
|
383
|
+
| Exact content | `"Bash(git add)"` | Only `git add` |
|
|
384
|
+
| Wildcard | `"Bash(git *)"` | Any git subcommand |
|
|
385
|
+
| File glob | `"Edit(src/*.ts)"` | TypeScript files under `src/` |
|
|
386
|
+
| Prefix (legacy) | `"Bash(npm:*)"` | `npm`, `npm install`, `npm run …` |
|
|
387
|
+
|
|
388
|
+
```jsonc
|
|
389
|
+
{
|
|
390
|
+
"hooks": {
|
|
391
|
+
"PreToolUse": [
|
|
392
|
+
{
|
|
393
|
+
"matcher": "Bash",
|
|
394
|
+
"hooks": [
|
|
395
|
+
{
|
|
396
|
+
"type": "command",
|
|
397
|
+
"command": ".coda/hooks/git-policy.sh",
|
|
398
|
+
"if": "Bash(git push*)"
|
|
399
|
+
}
|
|
400
|
+
]
|
|
401
|
+
}
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
For non-tool events, hooks with an `if` field are silently skipped.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Plugin hooks
|
|
412
|
+
|
|
413
|
+
Plugins can ship a `hooks/hooks.json` file at the plugin root. The format is identical to the `hooks` key in `config.json`, wrapped in a top-level object:
|
|
414
|
+
|
|
415
|
+
```jsonc
|
|
416
|
+
// hooks/hooks.json
|
|
417
|
+
{
|
|
418
|
+
"description": "Runs lint after every file write",
|
|
419
|
+
"hooks": {
|
|
420
|
+
"PostToolUse": [
|
|
421
|
+
{
|
|
422
|
+
"matcher": "write|edit",
|
|
423
|
+
"hooks": [
|
|
424
|
+
{
|
|
425
|
+
"type": "command",
|
|
426
|
+
"command": "${CODA_PLUGIN_ROOT}/scripts/lint.sh",
|
|
427
|
+
"timeout": 30
|
|
428
|
+
}
|
|
429
|
+
]
|
|
430
|
+
}
|
|
431
|
+
]
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Path placeholders in plugin hooks
|
|
437
|
+
|
|
438
|
+
| Placeholder | Resolves to |
|
|
439
|
+
| --- | --- |
|
|
440
|
+
| `${CODA_PLUGIN_ROOT}` | The plugin's installation directory |
|
|
441
|
+
| `${CODA_PROJECT_DIR}` | The current project directory (cwd) |
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Session environment files (`CODA_ENV_FILE`)
|
|
446
|
+
|
|
447
|
+
Hooks can export environment variables that persist across all subsequent Bash tool commands in the session.
|
|
448
|
+
|
|
449
|
+
### Mode 1 — External env file (process-level)
|
|
450
|
+
|
|
451
|
+
Set `CODA_ENV_FILE` in your shell before launching Coda. Its contents are prepended to every Bash command:
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
export CODA_ENV_FILE=~/.coda/my-env.sh
|
|
455
|
+
coda
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Mode 2 — Hook-writable env file (hook-level)
|
|
459
|
+
|
|
460
|
+
For `SessionStart` hooks (bash/wsl only), Coda sets `CODA_ENV_FILE` to a writable path before spawning your hook. Write `export` statements to that file; they are sourced before every subsequent Bash command in the session:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
#!/bin/bash
|
|
464
|
+
# SessionStart hook — activate a Python venv for the whole session
|
|
465
|
+
source .venv/bin/activate
|
|
466
|
+
echo "export VIRTUAL_ENV=$VIRTUAL_ENV" >> "$CODA_ENV_FILE"
|
|
467
|
+
echo "export PATH=$PATH" >> "$CODA_ENV_FILE"
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Async hooks
|
|
473
|
+
|
|
474
|
+
By default hooks run **synchronously** — the lifecycle event waits for every matched hook to complete. Two opt-in async modes are available:
|
|
475
|
+
|
|
476
|
+
### `async: true` — tracked background
|
|
477
|
+
|
|
478
|
+
The hook runs in the background. Results are polled at the start of each turn and applied (e.g. `additionalContext` is injected into the next tool result).
|
|
479
|
+
|
|
480
|
+
```jsonc
|
|
481
|
+
{
|
|
482
|
+
"type": "command",
|
|
483
|
+
"command": ".coda/hooks/slow-analysis.sh",
|
|
484
|
+
"async": true,
|
|
485
|
+
"timeout": 30
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### `asyncRewake: true` — detached watcher
|
|
490
|
+
|
|
491
|
+
The hook runs fully detached. If it exits with code 2, its stderr/stdout is injected as a system message that re-engages the model. Useful for background file watchers or integrity checkers.
|
|
492
|
+
|
|
493
|
+
```jsonc
|
|
494
|
+
{
|
|
495
|
+
"type": "command",
|
|
496
|
+
"command": ".coda/hooks/file-integrity-watcher.sh",
|
|
497
|
+
"asyncRewake": true
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Runtime async (hook-initiated)
|
|
502
|
+
|
|
503
|
+
A hook can decide at runtime to go async by printing `{"async": true}` as its first stdout line. Coda backgrounds the process immediately and continues.
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## Disabling all hooks
|
|
508
|
+
|
|
509
|
+
Set `disableAllHooks: true` in `config.json` to disable all hooks globally. Useful in CI environments or when debugging unexpected behavior:
|
|
510
|
+
|
|
511
|
+
```jsonc
|
|
512
|
+
{
|
|
513
|
+
"disableAllHooks": true
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Slash commands
|
|
520
|
+
|
|
521
|
+
| Command | What it does |
|
|
522
|
+
| --- | --- |
|
|
523
|
+
| `/hooks` | Browse all configured hooks (event → matchers → handlers) |
|
|
524
|
+
| `/hooks list [event]` | Filter the list by event name |
|
|
525
|
+
| `/reload-hooks` | Reload hooks config from disk without restarting the session |
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Environment variables available to hooks
|
|
530
|
+
|
|
531
|
+
| Variable | Value |
|
|
532
|
+
| --- | --- |
|
|
533
|
+
| `CODA_PROJECT_DIR` | Current project directory |
|
|
534
|
+
| `CODA_PLUGIN_ROOT` | Plugin installation directory (plugin hooks only) |
|
|
535
|
+
| `CODA_ENV_FILE` | Writable env file path (SessionStart hooks only, bash/wsl) |
|
|
536
|
+
|
|
537
|
+
All other environment variables from the Coda process are also inherited.
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## Execution model
|
|
542
|
+
|
|
543
|
+
- **All matching hooks run in parallel** (`Promise.all`). There is no guaranteed order between hooks that match the same event.
|
|
544
|
+
- **Mutation conflicts** (`updatedPrompt`, `updatedInput`, `additionalContext`) are resolved by **last-write-wins** across parallel results.
|
|
545
|
+
- **Any `blocked: true` result blocks the event** regardless of what other hooks returned.
|
|
546
|
+
- **Output is capped at 10 000 characters** per hook (stdout and stderr separately).
|
|
547
|
+
- **Default timeout is 600 seconds** per hook. Set `timeout` to a lower value for interactive hooks.
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Security considerations
|
|
552
|
+
|
|
553
|
+
- Hooks only run from config files you control (`~/.coda/config.json`, `.coda/config.json`) or plugins you have explicitly installed. They are not sandboxed.
|
|
554
|
+
- HTTP hook headers only interpolate env vars listed in `allowedEnvVars`.
|
|
555
|
+
- Review hook scripts before enabling them, especially from plugins or shared configs.
|
|
556
|
+
- Use `disableAllHooks: true` in environments where hooks should not run.
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## Common recipes
|
|
561
|
+
|
|
562
|
+
### Block `git push` in a project
|
|
563
|
+
|
|
564
|
+
```jsonc
|
|
565
|
+
{
|
|
566
|
+
"hooks": {
|
|
567
|
+
"PreToolUse": [
|
|
568
|
+
{
|
|
569
|
+
"matcher": "Bash",
|
|
570
|
+
"hooks": [
|
|
571
|
+
{
|
|
572
|
+
"type": "command",
|
|
573
|
+
"command": "jq -e '.tool_input.command | test(\"git push\")' > /dev/null && echo 'git push is blocked in this project' >&2 && exit 2 || exit 0",
|
|
574
|
+
"if": "Bash(git push*)"
|
|
575
|
+
}
|
|
576
|
+
]
|
|
577
|
+
}
|
|
578
|
+
]
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Run Prettier after every file write
|
|
584
|
+
|
|
585
|
+
```jsonc
|
|
586
|
+
{
|
|
587
|
+
"hooks": {
|
|
588
|
+
"PostToolUse": [
|
|
589
|
+
{
|
|
590
|
+
"matcher": "write|edit",
|
|
591
|
+
"hooks": [
|
|
592
|
+
{
|
|
593
|
+
"type": "command",
|
|
594
|
+
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -n \"$FILE\" ] && npx prettier --write \"$FILE\" 2>/dev/null; exit 0"
|
|
595
|
+
}
|
|
596
|
+
]
|
|
597
|
+
}
|
|
598
|
+
]
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Log every session to a file
|
|
604
|
+
|
|
605
|
+
```jsonc
|
|
606
|
+
{
|
|
607
|
+
"hooks": {
|
|
608
|
+
"SessionStart": [
|
|
609
|
+
{
|
|
610
|
+
"hooks": [
|
|
611
|
+
{
|
|
612
|
+
"type": "command",
|
|
613
|
+
"command": "echo \"$(date -Iseconds) session=$(jq -r .session_id) cwd=$(jq -r .cwd)\" >> ~/.coda/session-history.log"
|
|
614
|
+
}
|
|
615
|
+
]
|
|
616
|
+
}
|
|
617
|
+
]
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Notify a webhook on every prompt
|
|
623
|
+
|
|
624
|
+
```jsonc
|
|
625
|
+
{
|
|
626
|
+
"hooks": {
|
|
627
|
+
"UserPromptSubmit": [
|
|
628
|
+
{
|
|
629
|
+
"hooks": [
|
|
630
|
+
{
|
|
631
|
+
"type": "http",
|
|
632
|
+
"url": "https://hooks.example.com/coda-prompt",
|
|
633
|
+
"headers": { "Authorization": "$WEBHOOK_TOKEN" },
|
|
634
|
+
"allowedEnvVars": ["WEBHOOK_TOKEN"],
|
|
635
|
+
"timeout": 3,
|
|
636
|
+
"async": true
|
|
637
|
+
}
|
|
638
|
+
]
|
|
639
|
+
}
|
|
640
|
+
]
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Activate a Python venv for the whole session
|
|
646
|
+
|
|
647
|
+
```bash
|
|
648
|
+
#!/bin/bash
|
|
649
|
+
# .coda/hooks/activate-venv.sh
|
|
650
|
+
# Used as a SessionStart hook
|
|
651
|
+
if [ -f ".venv/bin/activate" ]; then
|
|
652
|
+
source .venv/bin/activate
|
|
653
|
+
echo "export VIRTUAL_ENV=\"$VIRTUAL_ENV\"" >> "$CODA_ENV_FILE"
|
|
654
|
+
echo "export PATH=\"$PATH\"" >> "$CODA_ENV_FILE"
|
|
655
|
+
fi
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
```jsonc
|
|
659
|
+
{
|
|
660
|
+
"hooks": {
|
|
661
|
+
"SessionStart": [
|
|
662
|
+
{
|
|
663
|
+
"hooks": [
|
|
664
|
+
{
|
|
665
|
+
"type": "command",
|
|
666
|
+
"command": ".coda/hooks/activate-venv.sh"
|
|
667
|
+
}
|
|
668
|
+
]
|
|
669
|
+
}
|
|
670
|
+
]
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
## Troubleshooting
|
|
678
|
+
|
|
679
|
+
**Hooks are not running**
|
|
680
|
+
- Check that `disableAllHooks` is not `true` in any config layer.
|
|
681
|
+
- Run `/hooks` to confirm the hook appears in the snapshot.
|
|
682
|
+
- Run `/reload-hooks` after editing config files.
|
|
683
|
+
- Verify the hook script is executable (`chmod +x`).
|
|
684
|
+
|
|
685
|
+
**Hook is blocking unexpectedly**
|
|
686
|
+
- A non-zero exit code other than 2 is non-blocking. Exit code 2 is the blocking signal.
|
|
687
|
+
- Check that your script does not accidentally exit with code 2 on success paths.
|
|
688
|
+
|
|
689
|
+
**Hook output is not reaching the model**
|
|
690
|
+
- `additionalContext` only works on `PostToolUse` hooks. Verify the event name in `hookSpecificOutput.hookEventName`.
|
|
691
|
+
- The hook must exit with code 0 and print valid JSON to stdout.
|
|
692
|
+
|
|
693
|
+
**Async hook results are not appearing**
|
|
694
|
+
- Async hook results are applied at the **start of the next turn**, not immediately.
|
|
695
|
+
- Check that `timeout` is long enough for the hook to complete.
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## See also
|
|
700
|
+
|
|
701
|
+
- [Configuration](configuration.md) — where `config.json` files live and how they merge
|
|
702
|
+
- [Features](features.md) — plugins, extensions, agents, skills
|
|
703
|
+
- [How to create a Coda extension](create-extension.md) — programmatic hooks via the extension API
|
|
704
|
+
- [Coda tools overview](tools.md) — tool names to use in `matcher` patterns
|