Hooks reference
This page lists every hook event Syntic Code fires, what each one sends to your hook, and what your hook can send back. For a gentler introduction with worked examples, start with the hooks guide.
Configuration format
Hooks live under a hooks key in any settings file, grouped by event name. Each event holds a list of matcher groups; each group holds a list of hooks.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./scripts/guard-shell.sh", "timeout": 15 }
]
}
]
}
}Hooks are read from ~/.syntic/settings.json, ~/.syntic/hooks.json, the project’s .syntic/settings.json, .syntic/hooks.json and .syntic/settings.local.json, your organization’s managed policy, and every enabled plugin’s hooks/hooks.json. All of them are combined. Project files only count once you trust the folder. An entry Syntic Code cannot parse is skipped; the rest still load.
Hook fields
| Field | Applies to | Meaning |
|---|---|---|
type | all | command (the default when left out), http, prompt or agent. |
command | command | The command line to run. |
args | command | A list of arguments. When present, command is started directly as a program with these arguments, without a shell. |
shell | command | bash (default; Git Bash on Windows) or powershell. |
async | command | true starts the hook in the background. The event does not wait, and the hook’s answer is ignored. |
asyncRewake | command | Implies async. If the background hook exits with code 2, its report is handed to the agent at its next step, or wakes an idle session. |
url | http | Where the event JSON is POSTed. |
headers | http | Extra request headers. Values may use $NAME or ${NAME} for variables listed in allowedEnvVars. |
allowedEnvVars | http | Environment variables that may be expanded in headers. Any other name expands to an empty string. |
prompt | prompt, agent | The instruction. $ARGUMENTS is replaced with the event JSON. |
model | prompt, agent | Model used to judge the event. |
timeout | all | Seconds before the hook is stopped. Default 60. |
statusMessage | all | Text shown on the working line while the hook runs. |
once | all | true runs the hook only the first time it matches in a session. |
if | tool events | A permission-rule pattern such as Bash(git push*). The hook only runs when the tool call matches it. |
Matchers
The matcher on a group is tested against one field of the event (listed per event below).
- Missing, empty or
*matches everything. - Letters, digits,
_and|only: an exact match against one of the|-separated names, for exampleEdit|Write. - Anything else is treated as a regular expression that must match the whole value, for example
mcp__github__.*. An invalid expression matches nothing. - Events without a matcher field run every group, whatever its
matchersays.
Common input fields
Each hook gets one JSON object. Command hooks read it on standard input, http hooks receive it as the request body, and prompt/agent hooks see it in place of $ARGUMENTS.
| Field | Present | Value |
|---|---|---|
hook_event_name | always | The event name, such as PreToolUse. |
session_id | always | The main session’s id, also inside a subagent. |
cwd | most events | The folder the next shell command would run in. |
permission_mode | most events | default, acceptEdits, auto, plan, bypassPermissions or dontAsk. |
transcript_path | when sessions are saved | Path of the session’s transcript.jsonl. |
agent_id, agent_type | inside a subagent | Which subagent fired the event. agent_type also appears when the main session runs as a custom agent (--agent). |
effort | when an effort level is set | { "level": "<level>" }. |
Events
Tool events
| Event | Fires | Matcher field | Extra input |
|---|---|---|---|
PreToolUse | Before a tool runs | tool_name | tool_name, tool_input, tool_use_id |
PostToolUse | After a tool succeeds | tool_name | Above, plus duration_ms and tool_response (the first 50,000 characters; tool_response_truncated: true when cut) |
PostToolUseFailure | After a tool returns an error | tool_name | tool_name, tool_input, tool_error, duration_ms |
PermissionRequest | Just before an approval dialog would show | tool_name | tool_name, tool_input, reason |
PermissionDenied | After a call is refused, by a rule, a hook or you | tool_name | tool_name, tool_input, reason |
Tool names are the ones on the Tools page; MCP tools are named mcp__<server>__<tool>.
Prompt and turn events
| Event | Fires | Matcher field | Extra input |
|---|---|---|---|
UserPromptSubmit | You send a message | none | prompt |
Stop | The agent is about to end its turn | none | stop_hook_active, last_assistant_message, background_tasks, session_crons |
StopFailure | A turn ends in an error | the error text | error |
MessageDisplay | A finished piece of reply text is about to be shown | none | message |
Notification | Syntic Code wants your attention | notification_type | message, and notification_type (permission_prompt when a tool needs approval; absent for the “finished its turn” notice) |
stop_hook_active is true when a Stop hook already sent the agent back this turn. Use it to avoid sending it back forever. background_tasks lists running background commands and agents; session_crons lists this project’s scheduled prompts.
Session events
| Event | Fires | Matcher field | Extra input |
|---|---|---|---|
Setup | With --init, --init-only or --maintenance, before SessionStart | trigger | trigger: init or maintenance |
SessionStart | Once, before the first turn | source | source: startup (interactive) or headless (-p) |
SessionEnd | When the session shuts down | reason | reason: exit (interactive) or headless-complete |
InstructionsLoaded | A memory or rules file is loaded | load_reason | file_path, memory_type (Managed, User, Local, Project), load_reason (session_start, nested_traversal, path_glob_match, include, compact), and when relevant trigger_file_path, parent_file_path, globs |
ConfigChange | A settings file changes on disk | source | source (user_settings, project_settings, local_settings, policy_settings), file_path |
CwdChanged | The shell’s working folder changes | none | cwd, previous_cwd |
FileChanged | A watched file is added, changed or removed | file name (the last part of the path) | file_path, event (add, change, unlink) |
DirectoryAdded | A working folder is added (/add-dir) | none | directory |
PreModelSwitch | The model is about to change | model | previous_model, model, source (session or host) |
PostModelSwitch | The model has changed | model | Same as above |
A FileChanged group’s matcher also tells Syntic Code which file names to watch, for example "matcher": ".envrc|.env". Hooks can add more files with watchPaths.
Compaction events
| Event | Fires | Matcher field | Extra input |
|---|---|---|---|
PreCompact | Before automatic compaction | trigger | trigger: auto, or prompt-too-long when a request overflowed the window |
PostCompact | After it | none | trigger, message_count |
Subagent, task and team events
| Event | Fires | Matcher field | Extra input |
|---|---|---|---|
SubagentStart | A subagent begins | agent_type | subagent_type, model |
SubagentStop | A subagent is about to finish | none | subagent_type, stop_hook_active, last_assistant_message, background_tasks, session_crons |
TaskCreated | The agent creates a task | none | task_id, task_subject, task_description |
TaskCompleted | A task is marked done | none | task_id, task_subject |
TeammateIdle | A teammate in an agent team runs out of work | none | teammate_name, team_name |
WorktreeCreate | A git worktree is about to be made | none | name, base_dir |
WorktreeRemove | A worktree is removed | none | worktree_path |
MCP events
| Event | Fires | Matcher field | Extra input |
|---|---|---|---|
Elicitation | An MCP server asks you a question | mcp_server_name | mcp_server_name, plus the server’s request (message, requestedSchema) |
ElicitationResult | The question has an answer | mcp_server_name | mcp_server_name, action (accept, decline, cancel), content |
Environment variables
Command hooks inherit your environment, minus Syntic Code’s own sign-in credentials, plus:
| Variable | Value |
|---|---|
SYNTIC_PROJECT_DIR | The project folder. |
SYNTIC_SESSION_ID | The session id. |
SYNTIC_EFFORT | The effort level, when one is set. |
SYNTIC_PLUGIN_ROOT, SYNTIC_PLUGIN_DATA | For plugin hooks: the plugin’s folder and its data folder. ${SYNTIC_PLUGIN_ROOT} and ${SYNTIC_PLUGIN_DATA} in command are replaced too. |
SYNTIC_USER_CONFIG_<KEY> | For plugin hooks: the plugin’s options. |
Output
Exit codes (command hooks)
| Code | Meaning |
|---|---|
0 | Success. Standard output is read as a JSON answer if it is one. For UserPromptSubmit, SessionStart and Setup, plain text output is added as context for the agent. |
2 | Block. Standard error becomes the reason, and the agent sees it. What “block” means depends on the event (see below). |
| anything else | A non-blocking error. It is reported and the session carries on. |
A hook that cannot start, runs past its timeout, or (for http) cannot be reached contributes nothing and never blocks. When a hook times out, its whole process group is stopped.
An http hook’s answer is the response body, in the same JSON shape a command prints. A non-2xx status is reported as a non-blocking error. A prompt or agent hook answers {"ok": true} or {"ok": false, "reason": "…"}; false blocks.
JSON fields
Print one JSON object on standard output for finer control.
| Field | Effect |
|---|---|
continue | false stops the whole run after this hook. |
stopReason | Shown to you when continue is false. |
decision | block (or deny) blocks. approve (or allow) approves. |
reason | Why. Shown to the agent on a block. |
systemMessage | A note for you, not the agent. |
suppressOutput | true hides the systemMessage notice. |
async | true declares the hook detached; its answer is ignored. |
reloadSkills | true rescans skill folders (SessionStart). |
terminalSequence | A bell, window title or desktop notification for Syntic Code to write to its terminal. Only BEL, OSC 0/1/2 titles and OSC 9/777/99 notifications are accepted; anything else is ignored. |
hookSpecificOutput | Event-specific fields, below. Its hookEventName must match the event or it is ignored. |
hookSpecificOutput fields
| Field | Events | Effect |
|---|---|---|
permissionDecision | PreToolUse, PreModelSwitch | allow, deny, ask or defer. For a model switch, ask asks you to confirm. |
permissionDecisionReason | PreToolUse | The reason shown with the decision. |
updatedInput | PreToolUse | Replacement tool input. A later hook’s rewrite wins. |
additionalContext | most events | Text handed to the agent with the next step. On Stop and SubagentStop, it sends the agent back with your feedback. |
updatedMCPToolOutput | PostToolUse | Replacement output for an MCP tool. |
decision | PermissionRequest | { "behavior": "allow", "updatedInput": … } or { "behavior": "deny", "message": "…", "interrupt": true }. Answers the dialog without showing it. |
watchPaths | SessionStart, CwdChanged, FileChanged | Files to watch for FileChanged. |
sessionTitle | SessionStart, UserPromptSubmit | Names the session. |
worktreePath | WorktreeCreate | Where the hook created the worktree. The last line of plain output also works. |
displayText, hideMessage | MessageDisplay | Show different text, or nothing. The agent still sees the original. |
action, content | Elicitation, ElicitationResult | Answer the server for you: accept with content, decline or cancel. |
What a block does
| Event | Effect |
|---|---|
PreToolUse | The tool does not run. The reason becomes the tool result. |
PostToolUse | The tool already ran; the reason is added to its result for the agent. |
UserPromptSubmit | Your message is not sent. |
Stop, SubagentStop | The agent keeps working, with the reason as its next instruction. At most 8 times in a row (SYNTIC_STOP_HOOK_BLOCK_CAP). |
TeammateIdle | The teammate is sent back to work with the reason, at most 3 times in a row. continue: false stops it. |
TaskCreated | The task is not created. |
ConfigChange | The settings change is not applied. |
PreModelSwitch | The model stays as it was. |
WorktreeCreate | The worktree is not created. |
Elicitation | The question is declined. |
StopFailure, SessionEnd, PreCompact, PostCompact, InstructionsLoaded, PermissionDenied and Notification only observe; their answers change nothing.
Hooks and permission rules
From strongest to weakest:
- A deny rule in settings always refuses the call.
- An ask rule always shows the dialog, even when a hook says
allow. - A
PreToolUsehook’sdenyrefuses the call. - A
PreToolUsehook’saskforces the dialog. - A
PreToolUsehook’sallowskips the dialog the mode would have shown. - Otherwise the normal flow decides. Before a dialog appears,
PermissionRequesthooks may answer it.
Limits
| Limit | Value |
|---|---|
| Default timeout | 60 seconds for every hook type |
tool_response sent to PostToolUse | 50,000 characters |
| Hook text passed to the agent | 10,000 characters; longer text is saved to a file and the agent gets the first 2,000 characters and the path |
| Stop hook continuations in a row | 8 (change with SYNTIC_STOP_HOOK_BLOCK_CAP) |
Turning hooks off
| Switch | Effect |
|---|---|
"disableAllHooks": true in your settings | Only organization hooks run. |
"disableAllHooks": true in managed policy | No hooks run. |
"allowManagedHooksOnly": true in managed policy | Only policy hooks run. |
"allowedHttpHookUrls" | http hooks run only when their URL matches one of these patterns. |
--bare | No hooks from settings files. |
SYNTIC_DISABLE_HOOKS=1 | No hooks at all. |
Command hooks run with your user rights. Read a repository’s hooks before you trust its folder.
Debugging
/hookslists every configured hook with its number and source file.syntic --debug hookslogs each hook run and its output.- In
-pruns,--output-format stream-json --verbose --include-hook-eventsreports each hook as it starts and finishes. - Test a script by hand:
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?.