Synonode Docs

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

EventWhen it runsWhat it can do
PreToolUseBefore 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.
PostToolUseAfter a tool ran.Add a note to the tool's result: a formatter's output, a lint error to fix.
UserPromptSubmitWhen you send a message, before Synonode reads it.Add context to it, or refuse it with a reason that is shown to you.
StopWhen Synonode is about to end its turn.Keep it working, with what is left to do (the tests still fail).
SubagentStopWhen an agent is about to hand back its report. The matcher is the agent's type.Send the agent back to work.
SessionStartWhen a conversation starts. The matcher is startup, clear (after /clear) or compact (after a compaction).Add context for the whole conversation.
SessionEndWhen /clear ends a conversation.Clean up, or keep a log.
NotificationWhen Synonode waits for you: a permission (permission_prompt) or a question or plan (elicitation_dialog).Notify you your own way.
PreCompact, PostCompactAround 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 UserPromptSubmit and SessionStart, 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}, or hookSpecificOutput with additionalContext, or for PreToolUse permissionDecision (allow, deny or ask) and permissionDecisionReason.

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.