API

Hook API

Push agent session events into the bar — Claude Code, Codex, or anything you wire in — with bidirectional approvals and questions.

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 sends approval.request or question.request, blocks reading, the user responds in the agent bubble, then the hook reads approval.resolve or question.resolve back 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)

tFieldsNotes
session.starts, 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.endsMarks session .completed; tile flashes & fades.
phases, 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.
events, kind (matches AgentEvent.Kind), text?Granular row in the popover transcript.
tool.starts, id (tool_use_id), name, args?Maintains pending-tools set; phase auto-flips to .toolRunning.
tool.ends, id, status (ok/error), text?Removes from pending; error flips state to .errored.
approval.requests, id, nameTile turns orange + shakes; on socket transport the connection blocks until response.
question.requests, id, questionsRenders 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.ends, durationMs?, stopReason?, tokens? ({in, out})Canonical "done" signal. Triggers 3-second green flash.
errors, messageSticky error state; clears on next event with kind=userMessage.

Outbound (ApexDock → hook, socket only)

tFieldsNotes
approval.resolves, id, decision (approved/denied)Sent in response to approval.request over the same connection.
question.resolves, id, answersSent 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:

bash
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).

bash
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.

bash
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

bash
# 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):

jsonc
{
  "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 hookApexDock event(s)
SessionStartsession.start
UserPromptSubmitevent (kind=userMessage) → phase (thinking)
PreToolUsetool.start, except AskUserQuestion sends question.request and waits for answers
PostToolUsetool.end (status from is_error)
PostToolUseFailuretool.end (status=error) — without this, failed tools never clear
Notificationevent (kind=system) — informational, not an approval signal
PermissionRequestapproval.request — the actual approval gate
Stop / SubagentStopturn.end
SessionEndsession.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

bash
# 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

ModeBehaviour
AutoBoth transports active. JSONL watcher + hooks; latest-event wins by sessionId.
JSONL onlyHook events for this provider are dropped. JSONL watcher continues.
Hooks onlyJSONL 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.resolve and question.resolve are socket-only.
  • Schema is v: 1. Future breaking changes will bump to v: 2.