You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -920,37 +920,150 @@ run = ["./scripts/bootstrap.sh"]
920
920
timeout_ms = 60000
921
921
922
922
[[projects."/Users/me/src/my-app".hooks]]
923
+
name = "lint-changed"
923
924
event = "tool.after"
924
925
run = "npm run lint -- --changed"
925
926
```
926
927
927
-
Supported hook events:
928
+
Hook configuration fields:
928
929
929
-
-`session.start`: after the session is configured (once per launch)
930
-
-`session.end`: before shutdown completes
931
-
-`tool.before`: immediately before each exec/tool command runs
932
-
-`tool.after`: once an exec/tool command finishes (regardless of exit code)
933
-
-`file.before_write`: right before an `apply_patch` is applied
934
-
-`file.after_write`: after an `apply_patch` completes and diffs are emitted
935
-
936
-
Hook commands run inside the same sandbox mode as the session and appear in the TUI as their own exec cells. Failures are surfaced as background events but do not block the main task. Each invocation receives environment variables such as `CODE_HOOK_EVENT`, `CODE_HOOK_NAME`, `CODE_HOOK_INDEX`, `CODE_HOOK_CALL_ID`, `CODE_HOOK_PAYLOAD` (JSON describing the context), `CODE_SESSION_CWD`, and—when applicable—`CODE_HOOK_SOURCE_CALL_ID`. Hooks may also set `cwd`, provide additional `env` entries, and specify `timeout_ms`.
930
+
| Field | Type | Notes |
931
+
| --- | --- | --- |
932
+
|`event`| string | Required. Hook event name (see list below). |
933
+
|`run`| string \| array<string> | Required. Command to execute. String form is parsed like a shell command. |
934
+
|`name`| string | Optional label used in logs and env vars. |
935
+
|`cwd`| string | Optional working directory for the hook (relative paths resolve from project cwd). |
936
+
|`env`| map<string,string> | Optional extra environment variables. |
937
+
|`timeout_ms`| number | Optional timeout in milliseconds (no timeout when omitted). |
938
+
|`run_in_background`| bool | Run without a TUI exec cell; still awaited and sandboxed. |
939
+
940
+
How hooks run:
941
+
942
+
- Hooks run in the order defined in the config. `CODE_HOOK_INDEX` reflects that order.
943
+
- Hooks run inside the same sandbox and approval mode as the session; hooks cannot request escalation.
944
+
- A hook may return control JSON to influence the workflow (details below). A hook may also stop further hooks for that event by returning `{"continue": false}` or exiting with status `2`.
945
+
- For `tool.after` and `file.after_write`, stdout/stderr in the payload are truncated to 2048 characters.
946
+
947
+
Common environment variables (available in every hook):
948
+
949
+
-`CODE_HOOK_EVENT`: canonical event name (e.g., `tool.after`).
-`CODE_HOOK_CALL_ID`: generated id for the hook exec.
952
+
-`CODE_HOOK_SUB_ID`: current session/turn id.
953
+
-`CODE_HOOK_INDEX`: 1-based index for this hook within the event.
954
+
-`CODE_HOOK_PAYLOAD`: JSON payload string describing the context.
955
+
-`CODE_SESSION_CWD`: project/session cwd.
956
+
-`CODE_HOOK_NAME`: the configured hook name (if provided).
957
+
-`CODE_HOOK_SOURCE_CALL_ID`: exec call id that triggered the hook (tool/file hooks only).
958
+
959
+
Common payload fields (inside `CODE_HOOK_PAYLOAD`):
960
+
961
+
-`event`: canonical event name.
962
+
-`session_id`: session UUID.
963
+
-`transcript_path`: path to the session transcript or `null` when unavailable.
964
+
-`cwd`: project/session cwd.
965
+
-`permission_mode`: `allow` or `ask` (based on the approval policy).
966
+
-`hook_event_name`: legacy-style event name (e.g., `PreToolUse`).
967
+
968
+
### Hook events
969
+
970
+
Each payload includes the common fields above plus the event-specific data below.
971
+
972
+
| Event | When it fires | Payload additions |
973
+
| --- | --- | --- |
974
+
|`session.start`| After session config completes (once per launch). |`sandbox_policy`, `approval_policy`. |
975
+
|`session.end`| Right before shutdown completes. |`sandbox_policy`, `approval_policy`. |
976
+
|`user.prompt_submit`| When the user submits a prompt. |`user_prompt`. |
977
+
|`tool.before`| Immediately before each exec/tool command. |`call_id`, `command`, `timeout_ms`, `cwd`. |
978
+
|`tool.after`| After each exec/tool command (success or failure). |`call_id`, `command`, `timeout_ms`, `cwd`, `exit_code`, `duration_ms`, `timed_out`, `stdout`, `stderr`, `success`, plus `tool_result` (same fields nested). |
979
+
|`file.before_write`| Right before an `apply_patch` is applied. |`call_id`, `command`, `timeout_ms`, `cwd`, `changes` (patch change list). |
980
+
|`file.after_write`| After an `apply_patch` completes. | Same as `file.before_write`, plus `exit_code`, `duration_ms`, `timed_out`, `stdout`, `stderr`, `success`. |
981
+
|`pre.compact`| Immediately before auto-compact runs. |`reason`. |
982
+
|`post.compact`| Right after auto-compact finishes. |`reason`. |
983
+
|`stop`| When a turn completes normally. |`reason` (e.g., `turn_complete`), `details` with `last_assistant_message`. |
984
+
|`subagent.stop`| When a sub-agent finishes. |`reason` (e.g., `subagent_complete`), `details` with `agent_id`, `agent_name`, `status`. |
985
+
|`notification`| Whenever a user notification is emitted. |`notification` (full `UserNotification` payload). |
937
986
938
987
Example `tool.after` payload:
939
988
940
989
```json
941
990
{
942
991
"event": "tool.after",
992
+
"session_id": "3e8b...",
993
+
"hook_event_name": "PostToolUse",
994
+
"permission_mode": "allow",
943
995
"call_id": "tool_12",
944
996
"cwd": "/Users/me/src/my-app",
945
997
"command": ["npm", "test"],
998
+
"timeout_ms": 120000,
946
999
"exit_code": 1,
947
1000
"duration_ms": 1832,
948
-
"stdout": "…output truncated…",
949
-
"stderr": "…",
950
-
"timed_out": false
1001
+
"timed_out": false,
1002
+
"stdout": "...truncated...",
1003
+
"stderr": "...truncated...",
1004
+
"success": false,
1005
+
"tool_result": {
1006
+
"exit_code": 1,
1007
+
"duration_ms": 1832,
1008
+
"timed_out": false,
1009
+
"stdout": "...truncated...",
1010
+
"stderr": "...truncated...",
1011
+
"success": false
1012
+
}
1013
+
}
1014
+
```
1015
+
1016
+
### Hook output (control JSON)
1017
+
1018
+
Hooks can print JSON to stdout or stderr to influence the flow. The parser accepts raw JSON or JSON wrapped in a fenced code block.
1019
+
1020
+
Supported keys:
1021
+
1022
+
-`continue`: `true`/`false` — stop running remaining hooks for this event when `false`.
1023
+
-`systemMessage`: string — appended as a system message (Chat API only).
1024
+
-`permissionDecision`: `allow`\|`ask`\|`deny` — gate `user.prompt_submit` or `tool.before`/`file.before_write`.
1025
+
-`hookSpecificOutput.permissionDecision`: same as above (preferred nesting for structured output).
1026
+
-`updatedInput`: modifies the incoming payload.
1027
+
- For `user.prompt_submit`, it can be a string, `InputItem`, or list of `InputItem`s.
1028
+
- For `tool.before`/`file.before_write`, it can be a string/array command or an object with `command`, `cwd`, `timeout_ms`, and `env`.
1029
+
1030
+
Example: prompt gate (used by `hooks/ask_user_prompt.py`)
-`scripts/run-hooks-test.sh`: launches Code with a temporary config that enables those hooks.
1057
+
1058
+
Test workflow:
1059
+
1060
+
```bash
1061
+
./build-fast.sh
1062
+
scripts/run-hooks-test.sh
1063
+
```
1064
+
1065
+
Then try a prompt like `hook: please summarize this repo` or run a shell command containing `rm -rf` to see the hook gate in action.
1066
+
954
1067
## Project Commands
955
1068
956
1069
Define project-scoped commands under `[[projects."<path>".commands]]`. Each command needs a unique `name` and either an array (`command`) or string (`run`) describing how to invoke it. Optional fields include `description`, `cwd`, `env`, and `timeout_ms`.
0 commit comments