The Hook API lets external tools (CLI agents, scripts, Claude Code's own hook system) push live session events into ApexDock's agent zone. Two transports share the same JSON schema:
- Unix socket at
~/Library/Application Support/ApexDock/api/api.sock. Required for interactive handshakes — the hook sendsapproval.requestorquestion.request, blocks reading, the user responds in the agent bubble, then the hook readsapproval.resolveorquestion.resolveback over the same connection. - File drop (one-way) — append a JSON line to
~/Library/Application Support/ApexDock/api/<sessionId>.jsonl. Stale files (>24h) are GC'd on launch.
Auth is filesystem permissions: parent dir is 700, socket is 600. Only the owning user's processes can connect.
Enable in Settings → Agents → Hook API. Built-in providers have source pickers (Auto / JSONL only / Hooks only); custom providers are accepted whenever the Hook API is enabled.
Schema
All events carry v: 1 (schema version) and s (session id). at is ISO-8601; defaults to "now" if omitted.
Inbound (hook → ApexDock)
t | Fields | Notes |
|---|---|---|
session.start | s, provider, cwd?, name?, parent?, icon?, wordmark?, color?, group? | Creates the tile. provider is free-form; claude-code and codex get built-in styling, anything else renders as a custom tile. icon accepts a filesystem path, a file:// URL, or a data:image/...;base64,... URL. wordmark sets chip text, color accepts #RRGGBB / RRGGBB, and group clusters related custom sessions. |
session.end | s | Marks session .completed; tile flashes & fades. |
phase | s, phase (thinking/toolRunning/idle), tool? | Sets the tile's status: thinking shows a slow pulse, toolRunning shows a fast pulse and the tool name in the chip, idle clears the pulse. |
event | s, kind (matches AgentEvent.Kind), text? | Granular row in the popover transcript. |
tool.start | s, id (tool_use_id), name, args? | Maintains pending-tools set; phase auto-flips to .toolRunning. |
tool.end | s, id, status (ok/error), text? | Removes from pending; error flips state to .errored. |
approval.request | s, id, name | Tile turns orange + shakes; on socket transport the connection blocks until response. |
question.request | s, id, questions | Renders a real question flow in the tile/popover. questions is Claude Code's AskUserQuestion array (question, header, multiSelect, options[]). Socket-only because answers must return to Claude. |
turn.end | s, durationMs?, stopReason?, tokens? ({in, out}) | Canonical "done" signal. Triggers 3-second green flash. |
error | s, message | Sticky error state; clears on next event with kind=userMessage. |
Outbound (ApexDock → hook, socket only)
t | Fields | Notes |
|---|---|---|
approval.resolve | s, id, decision (approved/denied) | Sent in response to approval.request over the same connection. |
question.resolve | s, id, answers | Sent in response to question.request over the same connection. answers is keyed by the original question text, matching Claude Code's AskUserQuestion result contract. |
approval.resolve and question.resolve over file-drop are not supported — file-drop is strictly one-way.
The apexdock-event helper
Bundled at ApexDock.app/Contents/Resources/bin/apexdock-event. Symlink it onto your $PATH:
ln -sf /Applications/ApexDock.app/Contents/Resources/bin/apexdock-event /usr/local/bin/apexdock-event
Picks the transport automatically (socket if available, file-drop otherwise) and handles JSON construction with proper escaping. approval.request is socket-only — blocks until the user resolves, exits 0 (approved) / 1 (denied) / 2 (transport error).
SID="$(uuidgen)"
apexdock-event session.start --session "$SID" --provider claude-code --cwd "$PWD" --name "$(basename "$PWD")"
apexdock-event phase --session "$SID" --phase thinking
apexdock-event tool.start --session "$SID" --id "tu-1" --name Bash --args "echo hi"
sleep 1
apexdock-event tool.end --session "$SID" --id "tu-1" --status ok --text "hi"
apexdock-event turn.end --session "$SID" --duration-ms 1234 --stop-reason end_turn --tokens-in 240 --tokens-out 80
apexdock-event session.end --session "$SID"
Custom providers
Anything except the built-in claude-code and codex provider names is treated as a custom provider. Provide wordmark for chip text, color for the accent, group to cluster related sessions, and icon when you want an image instead of the default symbol.
SID="$(uuidgen)"
apexdock-event session.start \
--session "$SID" \
--provider mcchicken \
--name "Mayo deploy" \
--wordmark "McD" \
--color "#FFC72C" \
--group "lunch" \
--icon /path/to/mcdonalds.png
If you omit styling fields, ApexDock uses a short wordmark from the provider name, a deterministic accent color, and the default custom-provider symbol.
Custom tile icons
# From an asset file
apexdock-event session.start --session "$SID" --provider claude-code --name MyTool --icon /path/to/icon.png
# From a bundled .app icon
apexdock-event session.start --session "$SID" --provider claude-code --name MyTool --icon "/Applications/MyTool.app/Contents/Resources/AppIcon.icns"
# Inline base64
apexdock-event session.start --session "$SID" --provider claude-code --name MyTool --icon "data:image/png;base64,iVBORw0KGgo..."
Claude Code integration
The bundled Swift CLI exposes apexdock claude-hook as the Claude Code hook entry point. It reads the hook JSON Claude pipes to stdin, sends Hook API events, and returns Claude-compatible hook responses for approvals and AskUserQuestion.
The one-time install is automatic from Settings → Agents → Install Claude Hooks. It writes a backup next to ~/.claude/settings.json, then points Claude at the bundled command.
Manual wiring (if you'd rather see the file):
{
"hooks": {
"SessionStart": [{"matcher": ".*", "hooks": [{"type": "command", "command": "'/Applications/ApexDock.app/Contents/Resources/bin/apexdock' claude-hook", "timeout": 3}]}],
"UserPromptSubmit": [{"matcher": ".*", "hooks": [{"type": "command", "command": "'/Applications/ApexDock.app/Contents/Resources/bin/apexdock' claude-hook", "timeout": 3}]}],
"PreToolUse": [{"matcher": ".*", "hooks": [{"type": "command", "command": "'/Applications/ApexDock.app/Contents/Resources/bin/apexdock' claude-hook", "timeout": 600}]}],
"PostToolUse": [{"matcher": ".*", "hooks": [{"type": "command", "command": "'/Applications/ApexDock.app/Contents/Resources/bin/apexdock' claude-hook", "timeout": 3}]}],
"PostToolUseFailure":[{"matcher": ".*", "hooks": [{"type": "command", "command": "'/Applications/ApexDock.app/Contents/Resources/bin/apexdock' claude-hook", "timeout": 3}]}],
"Notification": [{"matcher": ".*", "hooks": [{"type": "command", "command": "'/Applications/ApexDock.app/Contents/Resources/bin/apexdock' claude-hook", "timeout": 3}]}],
"Stop": [{"matcher": ".*", "hooks": [{"type": "command", "command": "'/Applications/ApexDock.app/Contents/Resources/bin/apexdock' claude-hook", "timeout": 3}]}],
"PermissionRequest":[{"matcher": ".*", "hooks": [{"type": "command", "command": "'/Applications/ApexDock.app/Contents/Resources/bin/apexdock' claude-hook", "timeout": 600}]}],
"SessionEnd": [{"matcher": ".*", "hooks": [{"type": "command", "command": "'/Applications/ApexDock.app/Contents/Resources/bin/apexdock' claude-hook", "timeout": 3}]}]
}
}
Claude hook → ApexDock event mapping
| Claude hook | ApexDock event(s) |
|---|---|
SessionStart | session.start |
UserPromptSubmit | event (kind=userMessage) → phase (thinking) |
PreToolUse | tool.start, except AskUserQuestion sends question.request and waits for answers |
PostToolUse | tool.end (status from is_error) |
PostToolUseFailure | tool.end (status=error) — without this, failed tools never clear |
Notification | event (kind=system) — informational, not an approval signal |
PermissionRequest | approval.request — the actual approval gate |
Stop / SubagentStop | turn.end |
SessionEnd | session.end |
PermissionRequest and PreToolUse need a long timeout (600). PermissionRequest blocks until you click Approve/Decline. PreToolUse normally returns immediately, but Claude's AskUserQuestion is delivered through PreToolUse, so ApexDock blocks until you answer the rendered question flow. For AskUserQuestion, the hook returns hookSpecificOutput.permissionDecision = "allow" with updatedInput.answers, which is the Claude Code contract for satisfying the interactive question.
After wiring, set Claude Code source to Hooks only (Settings → Agents → Hook API) to disable the JSONL watcher.
Manual smoke tests
# Socket — start a session and emit a phase
printf '{"v":1,"t":"session.start","s":"smoke-1","provider":"claude-code","cwd":"/tmp","name":"smoke"}\n{"v":1,"t":"phase","s":"smoke-1","phase":"thinking"}\n' \
| socat - UNIX:"$HOME/Library/Application Support/ApexDock/api/api.sock"
# File-drop — same effect, no socket needed
echo '{"v":1,"t":"session.start","s":"file-1","provider":"codex","cwd":"/tmp","name":"file"}' \
>> "$HOME/Library/Application Support/ApexDock/api/file-1.jsonl"
For a bidirectional flow, leave socat open after sending approval.request or question.request and watch approval.resolve / question.resolve arrive on stdout when you respond in the tile.
Source modes
| Mode | Behaviour |
|---|---|
| Auto | Both transports active. JSONL watcher + hooks; latest-event wins by sessionId. |
| JSONL only | Hook events for this provider are dropped. JSONL watcher continues. |
| Hooks only | JSONL watcher is stopped. Tiles only appear from hook events. |
Custom providers have no JSONL watcher or source picker in v1. They render only from Hook API events. When the master Hook API toggle is off, custom hook events are ignored.
Limitations (v1)
approval.resolveandquestion.resolveare socket-only.- Schema is
v: 1. Future breaking changes will bump tov: 2.