Hooks

Hooks let you run shell commands at specific points in the agent lifecycle. Use them to auto-format code after edits, run linters, send notifications, or enforce custom policies.

Overview

Hooks are configured in the hooks section of your creor.json. Each hook event maps to an array of hook entries. When the event fires, Creor runs each matching hook entry as a shell command.

1
2
3
4
5
6
7
8
9
10
11
{
"hooks": {
"tool.execute.after": [
{
"command": "npx prettier --write \"$HOOK_TOOL_OUTPUT\"",
"matcher": "Edit",
"description": "Auto-format after edit"
}
]
}
}

Hooks run in the project's working directory and inherit the system environment. They receive context via both environment variables and a JSON payload on stdin.

Note

Hooks added or modified via the UI or config file take effect immediately — no restart required. Creor reads the fresh config on every hook trigger.

Hook Events

The following lifecycle events are available:

EventWhen It FiresCan Block?
tool.execute.beforeBefore a tool runs. Receives tool name and input args.Yes
tool.execute.afterAfter a tool completes. Receives tool name and output.No
tool.execute.failureWhen a tool fails. Receives tool name and error.No
shell.envBefore any shell command. Hook stdout sets environment variables (KEY=VALUE format).No
command.execute.beforeBefore a slash command executes. Receives command name and arguments.Yes
chat.messageWhen a new chat message is sent. Receives session ID and agent name.Yes
session.startWhen a new session begins.No
session.endWhen a session ends. Always runs asynchronously.No
notificationWhen the agent sends a notification. Receives the message text.No
pre.compactBefore context compaction runs. Receives trigger type and message count.No

Hook Entry Config

Each hook entry supports the following fields:

FieldTypeDefaultDescription
commandstring(required)Shell command to execute
descriptionstringHuman-readable description shown in the UI
enabledbooleantrueSet to false to temporarily disable without removing
timeoutnumber30000Timeout in milliseconds. Hook is killed if it exceeds this.
matcherstring— (all tools)Tool name filter for tool.* events. Exact name, pipe-separated list ("Edit|Write"), or regex.
statusMessagestringMessage shown in the UI while the hook runs
asyncbooleanfalseFire-and-forget: don't wait for the hook to finish before continuing

Tip

Use async: true for hooks that should not block the agent loop — like sending Slack notifications or logging to an external service.

Environment Variables

Creor sets the following environment variables before running a hook command. The exact set depends on the event type:

VariableEventsDescription
HOOK_EVENTAllThe event name (e.g., "tool.execute.before")
HOOK_SESSION_IDMostCurrent session ID
HOOK_TOOL_NAMEtool.*Name of the tool being executed
HOOK_CALL_IDtool.execute.*Unique ID for this tool call
HOOK_TOOL_OUTPUTtool.execute.afterTool output (truncated to 8KB)
HOOK_TOOL_ERRORtool.execute.failureError message (truncated to 500 chars)
HOOK_PROJECT_DIRAllAbsolute path to the project directory
HOOK_CWDshell.envCurrent working directory for the shell command
HOOK_COMMANDcommand.execute.beforeSlash command name
HOOK_ARGUMENTScommand.execute.beforeCommand arguments
HOOK_AGENTchat.messageAgent name handling the message
HOOK_MESSAGEnotificationNotification message text (truncated to 500 chars)
HOOK_SOURCEsession.startSession start source (e.g., "startup")
HOOK_TRIGGERpre.compactCompaction trigger ("auto" or "manual")

JSON Payload

In addition to environment variables, Creor writes a JSON payload to the hook's stdin. This provides structured access to the same data plus any additional fields.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Example payload for tool.execute.before
{
"hook_event_name": "tool.execute.before",
"session_id": "abc123",
"tool_name": "Edit",
"tool_input": {
"file_path": "/path/to/file.ts",
"old_string": "const x = 1",
"new_string": "const x = 2"
},
"cwd": "/path/to/project",
"project_dir": "/path/to/project",
"timestamp": 1712345678000
}

You can read this payload in your hook script:

1
2
3
4
5
6
7
8
9
#!/bin/bash
# Read JSON from stdin
payload=$(cat)
tool_name=$(echo "$payload" | jq -r '.tool_name')
file_path=$(echo "$payload" | jq -r '.tool_input.file_path // empty')
 
if [ "$tool_name" = "Edit" ] && [ -n "$file_path" ]; then
npx prettier --write "$file_path"
fi

Blocking Hooks

Hooks on tool.execute.before, command.execute.before, and chat.message can block execution. There are two ways to block:

Exit Code 2

If the hook exits with code 2, Creor blocks the operation. The stderr output is used as the reason:

1
2
3
4
5
6
7
8
#!/bin/bash
if echo "$HOOK_TOOL_NAME" | grep -q "Bash"; then
if echo "$payload" | jq -r '.tool_input.command' | grep -q "rm -rf"; then
echo "Destructive rm -rf commands are not allowed" >&2
exit 2
fi
fi
exit 0

JSON Decision Output

Alternatively, print a JSON object to stdout with "decision": "block":

1
2
3
4
5
6
7
8
9
#!/bin/bash
payload=$(cat)
file_path=$(echo "$payload" | jq -r '.tool_input.file_path // empty')
 
if [[ "$file_path" == *"package-lock.json"* ]]; then
echo '{"decision": "block", "reason": "Do not edit package-lock.json directly"}'
exit 0
fi
echo '{"decision": "allow"}'

Modifying Tool Input

For tool.execute.before, the JSON output can also include an updatedInput field to modify the tool's arguments before execution:

1
2
3
4
#!/bin/bash
# Force all file writes to use LF line endings
payload=$(cat)
echo '{"updatedInput": {"line_endings": "lf"}}'

Examples

Auto-Format After Edit

1
2
3
4
5
6
7
8
9
10
11
12
{
"hooks": {
"tool.execute.after": [
{
"command": "npx prettier --write \"$HOOK_TOOL_OUTPUT\" 2>/dev/null || true",
"matcher": "Edit",
"description": "Run Prettier after file edits",
"timeout": 10000
}
]
}
}

Run Linter After File Write

1
2
3
4
5
6
7
8
9
10
11
12
{
"hooks": {
"tool.execute.after": [
{
"command": "npx eslint --fix \"$HOOK_TOOL_OUTPUT\" 2>/dev/null || true",
"matcher": "Edit|Write",
"description": "Run ESLint after file changes",
"timeout": 15000
}
]
}
}

Notify on Task Complete

1
2
3
4
5
6
7
8
9
10
11
{
"hooks": {
"notification": [
{
"command": "osascript -e 'display notification \"$HOOK_MESSAGE\" with title \"Creor\"'",
"description": "macOS notification on task complete",
"async": true
}
]
}
}

Block Dangerous Commands

1
2
3
4
5
6
7
8
9
10
11
{
"hooks": {
"tool.execute.before": [
{
"command": "payload=$(cat); cmd=$(echo \"$payload\" | jq -r '.tool_input.command // empty'); if echo \"$cmd\" | grep -qE 'rm -rf|DROP TABLE|truncate'; then echo 'Destructive command blocked' >&2; exit 2; fi",
"matcher": "Bash",
"description": "Block destructive shell commands"
}
]
}
}

Inject Environment Variables

1
2
3
4
5
6
7
8
9
10
{
"hooks": {
"shell.env": [
{
"command": "echo \"NODE_ENV=development\" && echo \"DEBUG=app:*\"",
"description": "Set dev environment variables for shell commands"
}
]
}
}

Log Session Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"hooks": {
"session.start": [
{
"command": "echo \"[$(date -Iseconds)] Session started: $HOOK_SESSION_ID\" >> ~/.creor/session.log",
"description": "Log session start",
"async": true
}
],
"session.end": [
{
"command": "echo \"[$(date -Iseconds)] Session ended: $HOOK_SESSION_ID\" >> ~/.creor/session.log",
"description": "Log session end"
}
]
}
}