Back to CLI

CLI Hooks

Consilium CLI hooks fire shell commands or HTTP webhooks at seven lifecycle events: SessionStart, SessionEnd, UserPromptSubmit, PreToolUse, PostToolUse, PermissionRequest, and Stop. Hooks let you enforce policies, audit activity, or trigger side effects from your own infrastructure. Configure them once in ~/.consilium/hooks.json and they run for every session.

Why hooks?

Hooks are the deterministic enforcement layer that complements model judgment. Anthropic's Claude Code documentation describes hooks as “a way to customize and extend Claude Code's behavior by registering shell commands.” Consilium adopts the same pattern but extends it with HTTP webhook support and a strict URL allowlist so corporate audit pipelines can subscribe without exposing the workstation to arbitrary destinations.

Hooks dispatch in under 10 ms of CLI overhead before your handler starts. The default per-hook timeout is 5 seconds. Slow handlers block the event they listen to, so a PreToolUse hook that takes 3 seconds will add 3 seconds to every tool call.

What lifecycle events can I hook into?

EventFires
SessionStartWhen a new chat session, debate, or REPL launches.
SessionEndWhen the session terminates (user quit, error, or completion).
UserPromptSubmitEvery time the user submits a prompt in chat or via a debate command.
PreToolUseBefore any built-in or MCP tool is invoked. Use this to enforce policies.
PostToolUseAfter a tool returns. Receives the tool result for logging or post-processing.
PermissionRequestWhen the CLI is about to request user approval for a sensitive operation.
StopWhen the user issues Ctrl-C or /stop. Final chance to flush state.

Where do I configure hooks?

Create or edit ~/.consilium/hooks.json. The CLI reads this file on every session start and on SIGHUP. The root must have hooksEnabled: true for any handler to fire.

{
  "hooksEnabled": true,
  "hooks": {
    "PreToolUse": [
      {
        "type": "command",
        "command": "~/.consilium/audit/check-tool.sh",
        "timeoutMs": 5000
      }
    ],
    "PostToolUse": [
      {
        "type": "http",
        "url": "https://audit.example.com/consilium/event",
        "method": "POST",
        "timeoutMs": 3000
      }
    ],
    "SessionStart": [
      { "type": "command", "command": "logger -t consilium session-start" }
    ]
  }
}

How do I write a command hook?

A command hook receives the event payload on stdin as JSON. Exit status determines whether the downstream action proceeds: 0 allows, 2 blocks with block: true, any other non-zero logs a warning but does not block. The example below blocks every Bash call to rm -rf.

#!/usr/bin/env bash
# ~/.consilium/audit/check-tool.sh
payload="$(cat)"
tool="$(echo "$payload" | jq -r '.toolName')"
input="$(echo "$payload" | jq -r '.toolInput.command // ""')"

if [[ "$tool" == "Bash" && "$input" == *"rm -rf"* ]]; then
  echo "blocked: rm -rf is not allowed by policy" >&2
  exit 2
fi
exit 0

How do I write an HTTP hook?

HTTP hooks POST the payload to your endpoint with content-type application/json. Your endpoint may reply with {block: true, message: '...'} to block, or any 2xx with empty body to allow. Non-2xx logs a warning and the action proceeds.

// Example Express handler at https://audit.example.com/consilium/event
app.post('/consilium/event', (req, res) => {
  const { event, toolName, toolInput, sessionId } = req.body;
  auditLog.write({ event, toolName, sessionId, ts: Date.now() });
  if (toolName === 'Bash' && /sudo|rm -rf/.test(toolInput?.command ?? '')) {
    return res.json({ block: true, message: 'destructive shell blocked' });
  }
  res.status(200).end();
});
HTTP URL allowlist (security)

HTTP hooks are restricted to URLs explicitly listed in allowedHookUrls in ~/.consilium/config.json. The default allowlist is empty, so HTTP hooks are opt-in per endpoint. This blocks supply-chain attacks where a hostile hooks.json (e.g. checked into a repo) tries to exfiltrate prompt content to an attacker-controlled host.

{
  "allowedHookUrls": [
    "https://audit.example.com/consilium/event",
    "https://hooks.slack.com/services/T000/B000/XXXX"
  ]
}

How do I block a tool call?

Three mechanisms:

  • Command hook: exit with code 2. The CLI interprets exit-code-2 as a hard block and surfaces the stderr output as the reason.
  • HTTP hook: return JSON {block: true, message: 'reason'}.
  • Hook chain: if any hook in the array for an event blocks, the downstream action does not proceed. Hooks run in array order; the first block short-circuits the chain.
Reference

Hook implementation lives in the public CLI repository: github.com/skadri1601/consilium-cli.

For the broader pattern, see Anthropic's Claude Code hooks documentation which defines the lifecycle vocabulary Consilium implements.