Make it yours
Hooks
Hooks run your own commands at set moments: before and after a tool, when you send a message, when Synonode is about to stop, when a conversation starts or ends, and when it waits for you. They work as Claude Code's do.
What hooks are for
A hook is a command in your project that Synonode runs at a set moment. Use them for rules the project must always follow, without saying them each time:
- Format a file after every edit (prettier, black).
- Refuse a command or an edit to a file that must not change.
- Keep Synonode working until the tests pass.
- Add today's facts to every message: the branch, the open ticket, the release date.
- Get a sound or a phone notification when Synonode waits for your answer.
Where they go
Hooks live in .synonode/hooks.json in the project. The file uses Claude Code's format, so hooks written for Claude Code work as they are:
{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "npx prettier --write \"$TOOL_FILE\"" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "sh scripts/tests-must-pass.sh", "timeout": 300 }] }
]
}
}
Synonode's shorter form also works: { "PreToolUse": [{ "matcher": "Bash", "command": "…" }] }. Type /hooks to see the project's hooks, and /doctor to check that the file is valid. A change to the file applies from the next step, without a restart.
The events
| Event | When it runs | What it can do |
|---|---|---|
PreToolUse | Before a tool runs. The matcher is the tool's name (Bash, Edit|Write). | Refuse it, with your reason going back to Synonode. Or decide its permission: allow it without asking, or ask you even in Auto. |
PostToolUse | After a tool ran. | Add a note to the tool's result: a formatter's output, a lint error to fix. |
UserPromptSubmit | When you send a message, before Synonode reads it. | Add context to it, or refuse it with a reason that is shown to you. |
Stop | When Synonode is about to end its turn. | Keep it working, with what is left to do (the tests still fail). |
SubagentStop | When an agent is about to hand back its report. The matcher is the agent's type. | Send the agent back to work. |
SessionStart | When a conversation starts. The matcher is startup, clear (after /clear) or compact (after a compaction). | Add context for the whole conversation. |
SessionEnd | When /clear ends a conversation. | Clean up, or keep a log. |
Notification | When Synonode waits for you: a permission (permission_prompt) or a question or plan (elicitation_dialog). | Notify you your own way. |
PreCompact, PostCompact | Around a compaction. The matcher is auto or manual. | Say what the summary must keep, or add something after it. |
What a hook gets, and how it answers
Each hook gets the details as JSON on its standard input, as in Claude Code: hook_event_name, cwd, session_id, and for its event tool_name and tool_input, tool_response, prompt, stop_hook_active, source or message. It runs in the project folder, with CLAUDE_PROJECT_DIR, TOOL_NAME, TOOL_FILE and TOOL_INPUT set.
- Exit 0: fine. For
UserPromptSubmitandSessionStart, what it prints is added for Synonode to read. - Exit 2: blocks. What it wrote to stderr is the reason: the tool is refused, the message is refused, or Synonode keeps working.
- Another exit code is the hook's own error. It does not block anything, except in
PreToolUse, where any failure refuses the tool. - JSON on stdout with exit 0 answers in Claude Code's words:
{"decision": "block", "reason": "…"},{"continue": false}, orhookSpecificOutputwithadditionalContext, or forPreToolUsepermissionDecision(allow,denyorask) andpermissionDecisionReason.
A Stop hook should read stop_hook_active and let go when it is already true, or Synonode would never stop. As a safety net, one turn is kept going at most 8 times. A hook that hangs is stopped after 30 seconds (or its own timeout, in seconds), and a hook that can't run counts as no hook.
Hooks run with whatever you can do on your computer, as commands do. Only add hooks you trust, as you would a script.