Skip to content

Hooks

Hooks are shell commands that run at lifecycle boundaries. They are configured under the hooks key in any settings file:

{
"hooks": {
"PreToolUse": ["./scripts/before_tool.sh"],
"PostToolUse": ["./scripts/log_tool.sh"],
"PostToolUseFailure": ["./scripts/alert.sh"],
"SessionStart": ["echo session started"],
"SessionEnd": ["./scripts/cleanup.sh"],
"UserPromptSubmit": [],
"Stop": [],
"PreCompact": [],
"PostCompact": ["./scripts/notify.sh"],
"PermissionRequest": [],
"PermissionDenied": ["./scripts/audit.sh"],
"Notification": []
}
}

All hook events

EventWhen it fires
SessionStartA new session is created (REPL launch or session.create over the bridge).
SessionEndSession exits cleanly.
UserPromptSubmitThe user submits a prompt; runs before the agent loop iterates.
PreToolUseBefore any tool call. Can allow / deny / ask / rewrite input.
PostToolUseAfter a tool call succeeds.
PostToolUseFailureAfter a tool call returns an error.
PreCompactBefore an auto- or manual-compaction runs.
PostCompactAfter compaction finishes.
PermissionRequestLime needs to prompt the user for a tool call.
PermissionDeniedA permission decision returned deny.
StopThe user cancels a turn (Esc / Ctrl-C).
NotificationA notification surfaces to the user (e.g. tunnel reconnect).

Stdin & environment

Each hook receives context two ways: as environment variables and as a JSON document on stdin.

VariableAlways setDescription
HOOK_EVENTThe event name (e.g. PreToolUse).
HOOK_TOOL_NAMEtool eventsThe tool being invoked (e.g. bash, write_file).
HOOK_TOOL_INPUTtool eventsThe tool’s JSON input as a string.
HOOK_TOOL_OUTPUTpost-eventsThe tool’s JSON output as a string.
HOOK_TOOL_IS_ERRORpost-events1 if the tool returned an error, otherwise 0.

The same payload (and more) is also delivered as JSON on stdin. The JSON form is the recommended interface — it carries fields that don’t fit cleanly into env vars (session id, turn id, request id, etc.).

PreToolUse control protocol

The most powerful hook. Its stdout can return JSON to control the tool call:

{
"systemMessage": "Suppressed by audit hook.",
"reason": "Hook policy",
"hookSpecificOutput": {
"permissionDecision": "deny",
"permissionDecisionReason": "git push to main is restricted",
"additionalContext": "see runbook RB-42",
"updatedInput": { "command": "git push origin feature/branch" },
"updatedPermissions": ["Bash(git push:*)"],
"interrupt": true
},
"interrupt": true,
"continue": false,
"decision": "block"
}
FieldEffect
hookSpecificOutput.permissionDecisionOne of allow, deny, ask. Forces a specific outcome.
hookSpecificOutput.permissionDecisionReasonShown in the audit log and the user prompt.
hookSpecificOutput.additionalContextAppended to the tool result for the model to see.
hookSpecificOutput.updatedInputReplaces the tool’s input before it executes.
hookSpecificOutput.updatedPermissionsAdds session-scoped allow rules.
hookSpecificOutput.interrupt / interruptCancel the current turn.
continue: false / decision: "block"Legacy aliases for “deny this call.”
systemMessageSurfaced to the user.
reasonSurfaced to the user and recorded in audit.

A non-JSON or empty stdout means “continue normally.”

Exit codes

CodeMeaning
0Continue normally (or apply whatever JSON instructs).
2Deny the tool call. Exit code 2 is treated as a hard deny even with no stdout.
any other non-zeroLogged as a hook error. The tool call proceeds — Lime is conservative about hook bugs blocking real work.

Failure semantics

  • A hook that crashes is logged but does not abort the call (except exit code 2, which denies).
  • A hook that hangs is killed after a per-event timeout.
  • PostToolUse and PostToolUseFailure are best-effort; their output is not awaited synchronously.
  • PreToolUse is awaited — it can block the call until it returns.

Useful patterns

Audit only — never block:

./scripts/log_tool.sh
#!/usr/bin/env bash
jq -c '. | {event: .event, tool: .tool_name, ok: (.is_error|not)}' \
>> ~/.lime/audit.log

Block destructive shell:

#!/usr/bin/env bash
# PreToolUse — refuses anything that looks like rm -rf
input="$HOOK_TOOL_INPUT"
if [[ "$HOOK_TOOL_NAME" == "bash" && "$input" == *"rm -rf"* ]]; then
cat <<'EOF'
{
"hookSpecificOutput": {
"permissionDecision": "deny",
"permissionDecisionReason": "rm -rf is blocked by hook"
}
}
EOF
exit 0
fi

Inject extra context:

#!/usr/bin/env bash
cat <<'EOF'
{
"hookSpecificOutput": {
"additionalContext": "Note: this repo uses pnpm, not npm."
}
}
EOF