Skip to content

Hooks

A hook is a shell command Polyglot runs at a specific point in the agent loop. It can block an action or add context to a prompt. Hooks let you enforce local policy - a command allowlist, a “don’t touch main” rule, a linter gate - without writing a plugin.

{
"hooks": {
"preToolUse": [
{ "command": "deny-dangerous-commands.sh", "tools": ["bash"] }
],
"userPromptSubmit": [
{ "command": "echo '{\"additionalContext\":\"The repo is frozen this week.\"}'" }
]
}
}
Event Fires A hook can
preToolUse a tool call is about to run - after the permission gate allows it block the call
postToolUse a tool result is about to reach the model - after secret scanning block the result
userPromptSubmit a message is about to start a turn block the turn, or append context to the message

preToolUse and postToolUse run for sub-agents too. preToolUse hooks can only tighten - they run after the gate has already allowed the call, so a hook can’t grant something the gate denied.

The hook is run with the working directory as cwd and POLYGLOT_HOOK_EVENT in its environment, and gets a JSON payload on stdin:

// preToolUse / postToolUse
{ "event": "preToolUse", "cwd": "", "toolName": "bash", "toolInput": { "command": "rm -rf /" } }
// postToolUse also has: "resultText", "isError"
// userPromptSubmit
{ "event": "userPromptSubmit", "cwd": "", "prompt": "" }

Exit code:

Code Meaning
0 proceed
2 block - stderr (or the JSON reason) is what the model / user sees
anything else non-blocking error - stderr is surfaced as a warning, the action proceeds

Optional stdout JSON (any subset):

{ "decision": "block", "reason": "rm is not allowed here", "additionalContext": "" }
  • decision: "block" blocks even on exit 0.
  • additionalContext (only useful for userPromptSubmit) is appended to the message inside a <context> block. Multiple hooks’ contexts are concatenated.

A simple linter-style hook needs none of this - just exit 2 when it wants to stop something.

#!/usr/bin/env bash
cmd=$(jq -r '.toolInput.command // ""')
case "$cmd" in
*" rm "*|"rm "*)
echo "rm is disabled by a hook - delete files yourself if you mean it" >&2
exit 2 ;;
esac
{ "hooks": { "preToolUse": [ { "command": "./hooks/no-rm.sh", "tools": ["bash"] } ] } }
HookSpec field
command the shell command (run via bash -c, or PowerShell on Windows)
tools glob-matched tool names this hook fires for (preToolUse / postToolUse only). Unset = every tool. e.g. ["bash", "web_*"]
timeoutMs kill the hook after this many ms (default 5000)

A hook that times out or crashes is treated as a non-blocking error - Polyglot warns and proceeds. Hooks are for convenience and soft policy; anything that must fail closed belongs in a custom permission gate, not a shell hook.

Security: project hooks are off by default

Section titled “Security: project hooks are off by default”

Hooks in a project’s .polyglot/settings.json are ignored unless your global ~/.polyglot/settings.json sets:

{ "hooks": { "allowProjectHooks": true } }

Cloning a repo and running polyglot in it must not execute that repo’s shell commands. When you do opt in, global and project hook lists are concatenated (global first).

POLYGLOT_NO_HOOKS=1 disables all hooks for a run. /status shows the configured counts (hooks: preToolUse(1) userPromptSubmit(1)). Every block is recorded in the audit log as a hook_blocked entry.