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
| Event | When it fires |
|---|---|
SessionStart | A new session is created (REPL launch or session.create over the bridge). |
SessionEnd | Session exits cleanly. |
UserPromptSubmit | The user submits a prompt; runs before the agent loop iterates. |
PreToolUse | Before any tool call. Can allow / deny / ask / rewrite input. |
PostToolUse | After a tool call succeeds. |
PostToolUseFailure | After a tool call returns an error. |
PreCompact | Before an auto- or manual-compaction runs. |
PostCompact | After compaction finishes. |
PermissionRequest | Lime needs to prompt the user for a tool call. |
PermissionDenied | A permission decision returned deny. |
Stop | The user cancels a turn (Esc / Ctrl-C). |
Notification | A 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.
| Variable | Always set | Description |
|---|---|---|
HOOK_EVENT | ✓ | The event name (e.g. PreToolUse). |
HOOK_TOOL_NAME | tool events | The tool being invoked (e.g. bash, write_file). |
HOOK_TOOL_INPUT | tool events | The tool’s JSON input as a string. |
HOOK_TOOL_OUTPUT | post-events | The tool’s JSON output as a string. |
HOOK_TOOL_IS_ERROR | post-events | 1 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"}| Field | Effect |
|---|---|
hookSpecificOutput.permissionDecision | One of allow, deny, ask. Forces a specific outcome. |
hookSpecificOutput.permissionDecisionReason | Shown in the audit log and the user prompt. |
hookSpecificOutput.additionalContext | Appended to the tool result for the model to see. |
hookSpecificOutput.updatedInput | Replaces the tool’s input before it executes. |
hookSpecificOutput.updatedPermissions | Adds session-scoped allow rules. |
hookSpecificOutput.interrupt / interrupt | Cancel the current turn. |
continue: false / decision: "block" | Legacy aliases for “deny this call.” |
systemMessage | Surfaced to the user. |
reason | Surfaced to the user and recorded in audit. |
A non-JSON or empty stdout means “continue normally.”
Exit codes
| Code | Meaning |
|---|---|
0 | Continue normally (or apply whatever JSON instructs). |
2 | Deny the tool call. Exit code 2 is treated as a hard deny even with no stdout. |
| any other non-zero | Logged 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.
PostToolUseandPostToolUseFailureare best-effort; their output is not awaited synchronously.PreToolUseis awaited — it can block the call until it returns.
Useful patterns
Audit only — never block:
#!/usr/bin/env bashjq -c '. | {event: .event, tool: .tool_name, ok: (.is_error|not)}' \ >> ~/.lime/audit.logBlock destructive shell:
#!/usr/bin/env bash# PreToolUse — refuses anything that looks like rm -rfinput="$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 0fiInject extra context:
#!/usr/bin/env bashcat <<'EOF'{ "hookSpecificOutput": { "additionalContext": "Note: this repo uses pnpm, not npm." }}EOF