API Reference

HTTP and WebSocket API served by irrlichd on localhost:7837.

Base URL

  • HTTP: http://127.0.0.1:7837
  • Unix socket: ~/.local/share/irrlicht/irrlichd.sock

Endpoints

GET /api/v1/sessions

Returns a hierarchical DashboardResponse with sessions grouped by project and nested parent-child relationships.

{
  "groups": [
    {
      "name": "irrlicht",
      "agents": [
        {
          "session_id": "52d087d1-...",
          "state": "working",
          "adapter": "claude-code",
          "project_name": "irrlicht",
          "pid": 12345,
          "subagents": { "total": 3, "working": 2, "waiting": 0, "ready": 1 },
          "children": [
            {
              "session_id": "agent-a1cad...",
              "state": "working",
              "parent_session_id": "52d087d1-...",
              "metrics": { "model_name": "claude-haiku-4-5", ... }
            }
          ],
          "metrics": {
            "model_name": "claude-opus-4-6",
            "context_utilization_percentage": 12.5,
            "pressure_level": "safe",
            "estimated_cost_usd": 0.18
          }
        }
      ]
    }
  ]
}

GET /state

Human-readable state summary. Shows count of working, waiting, and ready sessions, plus the total session count.

GET /api/v1/sessions/stream

WebSocket upgrade endpoint for real-time session state updates.

Message Types

Type Description
session_created New session detected
session_updated Session state or metrics changed
session_deleted Session removed
orchestrator_state Orchestrator state snapshot

Message Format

{
  "type": "session_updated",
  "session": { ... SessionState ... }
}

GET /api/v1/version

Returns the running daemon's version. Used by the web dashboard's header chip and by client integrations (the upcoming VS Code panel, future relay-served clients) to gate behavior on daemon capabilities.

{
  "version": "0.5.6"
}

GET /api/v1/agents

Returns the registered adapter branding so frontends can look up an adapter's display name and icon by name instead of hardcoding their own switches. Order mirrors allAgents in core/cmd/irrlichd/main.go; frontends should treat ordering as informational only and key by name. presets lists the backchannel command presets the adapter supports (empty when none); frontends render these as one-click actions in the backchannel UI.

[
  {
    "name": "claude-code",
    "display_name": "Claude Code",
    "icon_svg_light": "<svg ...>...</svg>",
    "icon_svg_dark":  "<svg ...>...</svg>",
    "presets": ["compact"]
  },
  {
    "name": "opencode",
    "display_name": "OpenCode",
    "icon_svg_light": "<svg ...>...</svg>",
    "icon_svg_dark":  "<svg ...>...</svg>",
    "presets": []
  }
]

GET /api/v1/history

Powers the History cost-analytics surface (cumulative spend, attribution drill-downs, concurrent-agents timeline, productive-vs-reverted yield). Reconstructed from the daemon's on-disk cost rollups and lifecycle recordings; returns an empty-but-valid payload (zeroed series) when no data is available, so the dashboard always renders.

Query parameters (all optional):

Param Values Description
chart cost (default), tokens, co2, models, providers, agents, state, yield, dora Which series to build. models / providers are presets that pin the stacking axis; co2 sums the estimated CO2e footprint like cost sums spend; agents returns the concurrent-agents timeline; state returns the per-project, per-state (working/waiting/ready) activity matrix; yield returns the per-project productive-vs-reverted aggregate; dora returns one project's DORA metrics (requires project)
group project (default), branch, provider, model, session, token_type Stacking dimension. token_type requires chart=tokens; agents/state are always grouped by project (recordings carry no other axis)
range day, week, month, year, this-month Time window. Alternatively pass start & end (unix seconds). Not used by chart=state, which resolves its window from granularity instead
granularity 1m, 10m, 60m, 8h, 24h (default), 7d, 1mo, 6mo, 1y chart=state only. Picks both the bucket width and the trailing window at once (e.g. 1m shows the last 45 minutes, 1y shows the last 8 years)
scope field:value Drilldown filter re-scoping the series to one contributor (field is one of project / branch / provider / model / session)
project, provider, token_type comma-separated Orthogonal cross-filters (the active group dimension is never filtered). token_type values: input / output / cache_read / cache_creation. For chart=dora, project instead names the single required repo (not a cross-filter)

The cost / tokens / co2 / models / providers / agents charts return a time-series payload (range, chart, group, start, end, bucket_seconds, bucket_starts, total, series, top_contributors, with optional forecast, token_split, and scope). chart=agents swaps in a concurrency block. chart=state is shaped differently — a dense grid rather than a sparse series — returning projects (row order, busiest first) and by_state (working/waiting/ready, each a project → per-bucket array map aligned to bucket_starts), plus the same concurrency block as agents (working+waiting combined). chart=yield returns a per-project shape (productive_cost, reverted_cost, unknown_cost, total_cost, yield, projects). chart=dora returns one project's four metrics (deployment_frequency, lead_time, change_failure_rate, mttr), each with value/unit/sample_size/available. Returns 400 for an unknown chart / group / token_type / granularity or an invalid range.

# Weekly cost by project
curl "http://127.0.0.1:7837/api/v1/history?chart=cost&group=project&range=week" | jq

# Activity Matrix: working/waiting/ready per project, last 24 hourly buckets
curl "http://127.0.0.1:7837/api/v1/history?chart=state&granularity=24h" | jq

GET /api/v1/permissions

Returns the consent-first permission snapshot: the permission mode (ask or grant-all), and for every agent its detection status plus each declared permission with its state (pending / granted / denied), title, what it touches, and the feature it unlocks. The macOS app and web dashboard drive the permission wizard from this endpoint.

curl http://127.0.0.1:7837/api/v1/permissions | jq
{
  "mode": "ask",
  "agents": [
    {
      "name": "claude-code",
      "detected": true,
      "permissions": [
        { "key": "hooks", "state": "granted", "title": "Install Claude Code hooks" }
      ]
    }
  ]
}

POST /api/v1/permissions/answer

Submits wizard answers: {"answers": [{"agent": "claude-code", "permission": "hooks", "grant": true}]}. Grants are exercised immediately (hook install, watcher start); revokes actively undo (hook uninstall, watcher stop). Returns the updated snapshot. First answer wins when both surfaces have the wizard open — the daemon broadcasts permissions_updated so the other surface dismisses.

POST /api/v1/sessions/{id}/focus

Activates the host terminal or IDE window of the given session, bringing it to the foreground. This is what the irrlicht-focus CLI calls when you click a session row or notification. The session ID is taken from the path; the request has no body. The daemon broadcasts the focus request and the macOS app activates the launching window.

Responses:

Status Meaning
200 Focus request broadcast (the Swift app activates the window)
400 Missing session ID in the path
404 Session not found
422 Session has no captured launcher information to focus
405 Method not allowed (only POST is accepted)
# Focus a session's terminal/IDE window
curl -X POST http://127.0.0.1:7837/api/v1/sessions/52d087d1-.../focus

POST /api/v1/sessions/{id}/input

Forwards text into a session's controlling terminal — the “backchannel” write path that lets Irrlicht act on an agent, not just observe it. Body: {"data": string}, a plain (JSON-escaped) byte string; control characters travel as JSON \u escapes (e.g. {"data":"hello\r"}). Input is injected as if you typed it, by scripting the terminal backend (tmux/kitty/…) that owns the session. Loopback only, and mutating cross-origin browser requests are rejected (Sec-Fetch-Site). Gated by the backchannel master toggle and the per-adapter control permission — both must be on.

Responses: 200 forwarded · 400 malformed request · 403 backchannel disabled or control consent not granted · 404 session not found · 405 non-POST · 409 session has no controllable terminal backend.

# Reply to a waiting agent
curl -X POST http://127.0.0.1:7837/api/v1/sessions/52d087d1-.../input \
  -d '{"data":"yes, proceed\r"}'

POST /api/v1/sessions/{id}/interrupt

Delivers an interrupt to a running turn (the read of a Ctrl-C into the terminal backend). No request body; same gating and same response codes as the input endpoint.

# Interrupt a running turn
curl -X POST http://127.0.0.1:7837/api/v1/sessions/52d087d1-.../interrupt

GET / POST / DELETE /api/v1/activation/backchannel

The default-OFF master toggle that gates the whole backchannel capability. GET reads the state, POST enables, DELETE disables, each replying {"backchannel_enabled": bool}. Loopback only; the mutating verbs reject cross-origin browser requests. Enabling the toggle is necessary but not sufficient — each agent still needs its control permission granted before input is forwarded.

GET / POST / DELETE /api/v1/activation/relay-control

Default-OFF toggle gating whether the relay forwarder acts on inbound control frames (the outer remote-control gate, for driving sessions from a relay-served client). Same verbs and loopback/cross-origin rules; replies {"relay_control_enabled": bool}. A standalone/headless daemon can instead enable it at startup via IRRLICHT_RELAY_CONTROL=on.

GET / POST / DELETE /api/v1/activation/task-eta

Legacy alias (issue #558) over the claude-code adapter’s instructions permission (issue #577), kept so the macOS Settings toggle’s wire shape stays stable. GET reads the state, POST grants, DELETE revokes, each replying {"task_eta_enabled": bool}. Loopback only; the mutating verbs reject cross-origin browser requests, since the granted effect rewrites ~/.claude/CLAUDE.md.

GET / PUT /api/v1/backchannel/rules

The event→action rule set (e.g. context pressure → /compact). GET returns the current rules; PUT replaces the set with {"rules": […]}. The rule engine fires through the same backchannel write path, so the same master-toggle + per-adapter control gates apply. Loopback only; PUT rejects cross-origin browser requests.

GET /api/v1/relay/publish

Reports the daemon’s outbound relay-publish state. When publishing is off it returns {"enabled":false}; when on, it returns the live forwarder link state: {"enabled":true,"url":...,"state":...,"daemonId":...,"daemonLabel":...} where state is one of connecting / connected / auth_failed / disconnected. The macOS app polls this to render the “Publishing” status dot.

PUT /api/v1/relay/publish

Reconfigures publishing on the running daemon (issue #722): {"enabled":bool,"url":string,"token":string}. The daemon starts, stops, or reconfigures its outbound forwarder live — no relaunch, no interruption to session monitoring — and returns the resulting status (same shape as the GET). Idempotent, so the app can re-send the current config freely. Loopback only (127.0.0.1): it mutates forwarder config and carries the relay token in its body, the same trust boundary as the other daemon-control endpoints. A standalone/headless daemon instead seeds publishing once at startup from IRRLICHT_RELAY_URL / IRRLICHT_RELAY_TOKEN.

# Turn on publishing to a relay
curl -X PUT http://127.0.0.1:7837/api/v1/relay/publish \
  -d '{"enabled":true,"url":"wss://relay.example.com","token":"..."}'

POST /api/v1/hooks/claudecode

Receiver for Claude Code hook events (the permission-request integration). Claude Code is configured to POST here on PermissionRequest, PreToolUse, PostToolUse, PostToolUseFailure, and PreCompact events; the daemon uses them to surface user-blocking state (permission gates, AskUserQuestion / ExitPlanMode overlays) and to force working during a manual /compact. Consent-gated behind the hooks permission — while pending or denied the payload is dropped with 200. The session is keyed off the transcript_path filename, not session_id.

Request body (only the fields the daemon reads; unknown fields are ignored):

Field Type Description
session_id string Claude Code session ID (informational; routing keys off transcript_path)
transcript_path string Absolute path to the session's JSONL transcript (required — its filename stem is the session ID)
hook_event_name string PermissionRequest, PreToolUse, PostToolUse, PostToolUseFailure, or PreCompact
tool_name string Tool involved in the event (e.g. AskUserQuestion, ExitPlanMode)
tool_use_id string ID of the tool call, when present
permission_mode string Current permission mode, when present
is_interrupt bool Whether the event is an interrupt
tool_input object Tool call input; scanned on PreToolUse for an in-band task-estimate marker
trigger string manual or auto on PreCompact events (the compaction cause); empty otherwise

Returns 200 with an empty body for recognized and ignored events (an empty PermissionRequest response means Claude Code shows its normal prompt). Returns 400 on invalid JSON or a missing transcript_path, and 405 for non-POST methods.

# Claude Code hook event (PermissionRequest)
curl -X POST http://127.0.0.1:7837/api/v1/hooks/claudecode \
  -H 'Content-Type: application/json' \
  -d '{
    "session_id": "52d087d1-...",
    "transcript_path": "/Users/me/.claude/projects/proj/52d087d1-....jsonl",
    "hook_event_name": "PermissionRequest",
    "tool_name": "Bash"
  }'

POST /api/v1/hooks/claudecode/statusline

Receiver for Claude Code's per-tick statusline JSON. Claude Code is configured (via settings.json's statusLine.command) to pipe its statusline payload to a curl that POSTs here. The handler extracts the rate_limits block — subscription quota data available only to Claude.ai Pro/Max accounts — and routes it to the matching session's metrics. API-key, Bedrock, and Vertex users send no rate_limits block; the tick is acknowledged and nothing is recorded. Consent-gated behind the statusline permission — while pending or denied the payload is dropped with 200.

Request body:

Field Type Description
session_id string Claude Code session ID (informational; routing keys off transcript_path)
transcript_path string Absolute path to the session's JSONL transcript (required)
rate_limits object Optional. Subscription quota, with five_hour and seven_day windows (each {"used_percentage": float, "resets_at": unix-seconds})

Returns 200 with an empty body on success (and when there is no rate_limits block to record). Returns 400 on invalid JSON or a missing transcript_path, and 405 for non-POST methods.

# Claude Code statusline tick (Pro/Max account)
curl -X POST http://127.0.0.1:7837/api/v1/hooks/claudecode/statusline \
  -H 'Content-Type: application/json' \
  -d '{
    "session_id": "52d087d1-...",
    "transcript_path": "/Users/me/.claude/projects/proj/52d087d1-....jsonl",
    "rate_limits": {
      "five_hour": { "used_percentage": 16,   "resets_at": 1778761800 },
      "seven_day": { "used_percentage": 14.0, "resets_at": 1779188400 }
    }
  }'

GET /debug/bundle

Collects a redacted diagnostics snapshot for bug reports and returns it as a gzipped tar (Content-Disposition: attachment; filename="irrlicht-diag-<version>.tar.gz"). The bundle captures the daemon's resolved stores and config with sensitive values redacted. Loopback only. Headless installs that can't hit this route can produce the same bundle from the CLI with irrlichd --diagnose, which writes irrlicht-diag.tar.gz to the current directory without starting the daemon.

# Download a diagnostics bundle
curl -OJ http://127.0.0.1:7837/debug/bundle

GET /

Serves the web dashboard UI from disk — resolveUIDir in core/cmd/irrlichd/paths.go walks up from the executable to find platforms/web/index.html, with the production .app bundle layout and the curl --daemon-only install path as fallbacks.

SessionState Schema

Field Type Description
session_id string Unique session identifier (UUID or proc-<pid>)
state string working, waiting, or ready
adapter string One of claude-code, codex, pi, aider, opencode, kiro-cli, gemini-cli, antigravity, mistral-vibe, or empty (legacy / unknown)
cwd string Working directory of the session
transcript_path string Absolute path to the JSONL transcript file
git_branch string Current git branch name
project_name string Project name derived from git root
pid int Process ID of the agent
first_seen int64 Unix timestamp when the session was first detected
updated_at int64 Unix timestamp of the last state update
metrics object SessionMetrics object (see below)
parent_session_id string ID of the parent session, if this is a sub-agent
subagents object SubagentSummary — aggregate state of all child sessions (see below)
daemon_version string Version of the running daemon

SubagentSummary Schema

Present on parent sessions that have active or recently-active child sessions (subagents). Computed by BuildDashboard by merging in-process agents (open Agent tool calls) with file-based child sessions.

Field Type Description
total int Total number of child sessions (in-process + file-based)
working int Count of children in working state
waiting int Count of children in waiting state
ready int Count of children in ready state

SessionMetrics Schema

Field Type Description
elapsed_seconds int Seconds since session started
total_tokens int64 Total tokens consumed (input + output)
model_name string Normalized model name
context_window int64 Total context window size for the model
context_utilization_percentage float64 Percentage of context window used
pressure_level string Context pressure level (safe, caution, warning, critical)
has_open_tool_call bool Whether the agent has an open tool call
open_tool_call_count int Number of currently open tool calls
last_event_type string Type of the most recent transcript event
last_open_tool_names []string Names of currently open tools
last_was_user_interrupt bool Whether the most recent user event was an ESC cancellation
estimated_cost_usd float64 Estimated session cost in USD
task_estimate object The agent's self-reported task progress (total_rounds, completed_rounds, updated_at, source), parsed from its in-band progress marker or derived from the task list / subagents
task_completion_eta int64 Projected unix-seconds completion time for the current task, derived from the measured progress rate; absent when no progress has been reported

WebSocket Client Example

const ws = new WebSocket('ws://127.0.0.1:7837/api/v1/sessions/stream');
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  console.log(msg.type, msg.session?.session_id);
};

curl Examples

# List sessions
curl http://127.0.0.1:7837/api/v1/sessions | jq

# Quick status
curl http://127.0.0.1:7837/state

# Via unix socket
curl --unix-socket ~/.local/share/irrlicht/irrlichd.sock \
  http://localhost/api/v1/sessions