zot-cluade-hooks is a zot extension that runs command hooks defined in Claude-style JSON settings files. It lets existing PreToolUse hooks participate in zot tool calls and forwards the lifecycle events that zot currently exposes.
The repository and extension name intentionally use
cluadefor compatibility with the existing package and manifest names.
You need:
- Bun, to install and run this extension.
zot, available on yourPATH.- A project directory in which zot can run.
From this repository, install the dependency:
bun installCreate .claude/settings.json in the project where you run zot:
{
"hooks": {
"PreToolUse": [
{
"matcher": "^bash$",
"hooks": [
{
"type": "command",
"command": "sh .zot/hooks/check-bash.sh"
}
]
}
]
}
}Create the command referenced above at .zot/hooks/check-bash.sh:
#!/bin/sh
set -eu
payload=$(cat)
printf 'checking %s\\n' "$payload" >&2
# Exit 0 to allow the tool call.
exit 0Make it executable if you invoke it directly, or leave it non-executable when calling it through sh:
chmod +x .zot/hooks/check-bash.shThe hook receives one JSON object on standard input. For a PreToolUse hook, it includes hook_event_name, cwd, tool_name, and tool_input.
Run zot from the project directory:
zot --ext /path/to/zot-claude-hooksAsk zot to use the bash tool. The hook runs before the tool call. Because the matcher is ^bash$, calls to other tools are not matched.
Change the script to return exit status 2:
#!/bin/sh
set -eu
printf '%s\\n' '{"decision":"block","reason":"bash is disabled for this project"}'
exit 2The tool call is blocked and zot receives the supplied reason. A JSON response with "decision":"block" also blocks the call; exit status 2 is the command-level blocking signal.
Run the extension's diagnostic command from the project directory:
bun run listEach discovered hook is printed as a tab-separated row containing its event, matcher, source file, and command. This command uses the same discovery logic as the zot extension process.
Create .zot/zot-cluade-hooks.json:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "sh .zot/hooks/on-session-start.sh",
"timeout": 5
}
]
}
]
}
}timeout is specified in seconds. The default is 10 seconds. The command is terminated when its timeout expires.
A hook can read its JSON payload from standard input and append it to a file:
#!/bin/sh
set -eu
mkdir -p .zot
cat >> .zot/hook-events.jsonlKeep hook diagnostics on standard error. The extension uses standard output for its JSONL protocol when it is running under zot; arbitrary output from the extension process can break the protocol.
For protocol-level debugging, set ZOT_HOOKS_PROTOCOL_TRACE to a writable file. The extension writes one JSON object per line with the direction (in or out) and protocol frame:
ZOT_HOOKS_PROTOCOL_TRACE=/tmp/zot-hooks-protocol.jsonl \
zot --ext /path/to/zot-cluade-hooksSet ZOT_HOOKS_PATH to a JSON file or a path relative to the project directory:
ZOT_HOOKS_PATH="$HOME/.config/zot/hooks.json" \
zot --ext /path/to/zot-cluade-hooksZOT_USER_CONFIG_DIR is another option for a user configuration directory. The extension looks for zot-cluade-hooks.json below that path.
Run unit and integration tests with Bun:
bun test
bun run test:e2eThe end-to-end tests launch zot with temporary configuration and a fake provider. They verify allowing a matching hook and blocking with either exit status 2 or a JSON decision.
Manual fixtures are documented in fixtures/README.md.
Every discovered JSON file must contain a top-level hooks object. Each event maps to an array of hook groups. A group may provide a regular-expression matcher and contains an array of command hooks:
{
"hooks": {
"PreToolUse": [
{
"matcher": "^(bash|edit)$",
"hooks": [
{
"type": "command",
"command": "./.zot/hooks/validate.sh",
"timeout": 10
}
]
}
]
}
}Supported command fields:
| Field | Required | Meaning |
|---|---|---|
type |
yes | Must be "command". Other hook types are ignored. |
command |
yes | Shell command executed with the project directory as its working directory. |
timeout |
no | Maximum runtime in seconds. Defaults to 10; values are clamped to at least 0.1 seconds. |
matcher |
no | JavaScript regular expression matched against the zot tool name. Defaults to .*. |
Relative command paths and relative configuration paths resolve from the project directory. Invalid regular expressions are logged and do not match. A malformed or unreadable configuration file is logged and skipped.
Existing files are checked in this order:
~/.claude/settings.json.claude/settings.json.claude/settings.local.json$ZOT_USER_CONFIG_DIR/zot-cluade-hooks.json(whenZOT_USER_CONFIG_DIRis set).zot/zot-cluade-hooks.json.zot/zot-cluade-hooks.local.json$ZOT_HOOKS_PATH(whenZOT_HOOKS_PATHis set)
All valid definitions found at these paths are loaded. Later files do not automatically replace earlier files, so use matchers and commands that make multiple matching hooks safe.
The compatibility matrix below shows how zot lifecycle events map to hook names and whether this extension currently supports them:
| zot event | Claude hook | zot-claude-hooks support |
|---|---|---|
session_start |
SessionStart |
Supported |
user_prompt_submit |
UserPromptSubmit |
Not supported: zot does not currently expose this event to extensions. |
turn_start |
ā | Available in zot, but there is no direct hook equivalent currently implemented. |
tool_call |
PreToolUse |
Supported synchronously; exit 2 or JSON decision: "block" prevents the call. |
tool_result |
PostToolUse |
Not supported: zot does not currently expose post-tool results to extensions. |
tool_confirmation_requested |
PermissionRequest |
Observable in zot, but not currently mapped to a hook. |
permission_decision |
PermissionRequest |
Not supported: zot does not currently expose the final permission decision. |
turn_end |
Stop |
Supported. Runs when a turn ends. |
assistant_message |
Notification |
Partially supported through the currently available notification-like events. |
session_end |
SessionEnd |
Not supported: zot does not currently expose session shutdown to extensions. |
pre_compact |
PreCompact |
Not supported. |
post_compact |
ā | Not supported. |
subagent_start |
SubagentStart |
Not supported. |
subagent_stop |
SubagentStop |
Not supported. |
The current implementation supports these Claude-style event names:
| Hook event | zot source | Behaviour |
|---|---|---|
PreToolUse |
synchronous tool_call interception |
Runs before the tool. Exit 2 or JSON decision: "block" prevents the call. |
SessionStart |
session_start |
Runs when the extension session starts. |
Stop |
turn_end |
Runs when a turn ends. |
Notification |
tool_call and assistant_message events |
Runs for the currently available notification-like events. |
PreToolUse receives a payload such as:
{
"hook_event_name": "PreToolUse",
"cwd": "/path/to/project",
"tool_name": "bash",
"tool_input": { "command": "printf hello" }
}Asynchronous event hooks receive the zot event frame with hook_event_name added. Their output does not change the event; use them for auditing, notifications, or other side effects.
- Exit status
0allows aPreToolUsecommand to continue. - Exit status
2blocks the tool call. - JSON output containing
{"decision":"block","reason":"..."}blocks the tool call and supplies the reason. - Plain-text output is not a structured decision.
- Timeouts and invalid responses fail open, except for a valid exit status
2. - Hook standard error is forwarded to the extension diagnostics.
The extension is a JSONL protocol process described by extension.json. When zot starts it, the process sends a hello frame, receives hello_ack, loads hook files using the working directory, subscribes to the events available in the current zot protocol, and reports ready.
For a tool call, zot sends an interception frame. The extension selects PreToolUse commands whose matcher matches the tool name, passes each command the Claude-style payload, and returns an interception response. Other subscribed events are forwarded asynchronously to matching commands.
This repository currently implements the protocol client, hook discovery, PreToolUse, and the event forwarding listed above. It does not yet implement every Claude hook event.
Planned hook and lifecycle support is tracked in PLAN.md:
| Planned capability | Current status |
|---|---|
user_prompt_submit |
Waiting for zot to expose the event. |
PostToolUse / tool_result |
Waiting for zot tool-result events. |
| Final tool status | Planned distinction between completed, failed, blocked, cancelled, and timed-out calls. |
session_end |
Planned. |
pre_compact and post_compact |
Planned. |
subagent_start and subagent_stop |
Planned. |
permission_decision |
Planned. |
| Prompt replacement or synchronous prompt blocking | Waiting for zot semantics. |
The plan also includes event-ordering tests and reconsidering fail-open behaviour if zot adds a fail-closed policy mode.
Hook files execute arbitrary shell commands from user and project configuration. Review configuration before enabling it, especially in untrusted repositories. The extension intentionally does not add a trust prompt yet; that remains an open design decision.
extension.json: zot extension manifest.index.ts: JSONL protocol process, hook discovery, command runner, andlistdiagnostic command.fixtures/README.md: manual fixture walkthroughs.test/e2e/runner.test.ts: end-to-end coverage.PLAN.md: current capabilities, limitations, and future work.package.json: Bun scripts and dependency declaration.
This is an early 0.1.0 extension scaffold. The current behaviour is defined by the implementation and tests in this repository; use PLAN.md for the boundary between available functionality and planned work.