Skip to content

About

Live observability for agent swarms in VS Code — Claude Code and OpenCode | The screen developers stare at while their agents work.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Agent Deck

Agent Deck on the VS Code Marketplace

VS Code extension · Open source · 0.9.2

Live observability for agent swarms, inside VS Code. When a coding agent spawns subagents, the terminal shows you one scrolling column and no shape. Agent Deck shows you the shape: every session on the machine, the tree of agents inside each one, which agent spawned which, what each is running right now, and what it has cost. It works with Claude Code, OpenCode and Codex, side by side in one panel. It observes only — it never wraps, launches, proxies or configures any of them.

It also keeps the facts: which files a session touched, which calls it repeated, how its tokens moved, in a Stats view and a local history on your machine — numbers, never session content, and never advice.

Agent Deck: one panel, the sessions of the folders this window has open, live

Claude Code compatibility — anchor 2.1.246, accepts 2.0.x to 2.2.x, refuses on structural change, not on patch number. A session imported from another machine — Claude Code's --teleport — is not supported and renders unsupported. See Claude Code version window.

OpenCode compatibility — anchor 1.18.22, accepts 1.17.x to 1.19.x, same rule: the patch number is not compared, and what refuses is the schema. See Also observes OpenCode.

Codex compatibility — anchor 0.151.0-alpha.7.2, accepts 0.150.x to 0.152.x, same rule: neither the patch component nor the prerelease tag is compared, and what refuses is the structure. See Also observes Codex.


What you see

The deck — one cell per session, from any of the three engines, scoped to the folders this window has open. Cells breathe while their session is working. Three layouts (List, Grid, Lanes), three sort orders (Live first, Recent, Engine), and chips to filter by liveness or by engine. Keyboard: A C O X, 1 2 3, L R E.

The deck in Grid layout: one cell per session, each with its engine, agents, calls in flight, tokens and liveness

The tree — one session's interior. Every agent is a node; children sit under the parent that spawned them, in spawn order; a filament runs from each parent to every agent it spawned. A node pulses while one of its tool calls is in flight and stops when the call ends, so the picture tells you what is happening now, not only what happened. A parent with many children lays them out in rows rather than one line running off the panel, and nothing is ever cut short with an ellipsis — a long label wraps and carries its full text on hover. Anything that cannot be attached to a parent goes to a parked rail carrying the reason, because unplaced data is shown as unplaced and never guessed into position.

One session's tree: the root agent and the two subagents it spawned, with tokens and calls per agent

Focus — click any agent to re-root the tree on it and read one branch of a wide run on its own. The breadcrumb walks back out; Reset view returns to the whole session, fitted.

The inspector — a drawer along the bottom, the width of the panel. Its header carries the selected node's status, its numbers and its duration; below, every tool call in that agent is listed with its status, the child it spawned, the time since the agent's first call and the gap between calls, oldest-first or newest-first, filterable by tool. Select a row to read its payload beside the list. Show details / Hide details collapses the payload and close dismisses a row. An oldest-first list follows new calls as they arrive until you open one or scroll away.

The inspector under the tree: the root agent's four tool calls, one of them expanded to its input and output

The tool-call drawer under a session's tree: each call with its time since the agent's first call and the gap since the call before it

Stats — a view mode beside the canvas and the list: files by touch count, identical-call loops and churn chains, tokens per agent, and trends across the sessions stored on this machine. Facts only; what each term means is in Stats.

The sidebar carries a strip of three tabs, one open at a time: Menu (Open Deck, Open Statistics, Open Insights, Pick Agent, Show Payload, Clear History, Show Diagnostics, Settings, Clear Stats History, About), where the three after Open Insights appear only while an Insights provider offers them.

Two numbers, and a third where the engine states one. Context is the last message's prompt — a level, what is in the window now, which goes up and down. Burn is the running total across the session — it only goes up. Window sits beside them and is read from the session itself: a Codex transcript states the model's context window, Claude Code's and OpenCode's do not, so for those two it reads as an em dash rather than a guess. There is no percentage anywhere — two of the three engines report no window at all, and deriving one from a model name would be a number we made up. Cost is rendered where it belongs on the tree; there are no cost dashboards.

Trust

Agent Deck observes. It never acts.

  • Read-only. It never writes to your agents' settings, your session files, or anything under ~/.claude, OpenCode's data and config directories, or Codex's data root. Installing the hooks is a manual paste block you control, below. Zero write capability is the trust anchor, not a default that could be configured away.
  • Zero network egress. Agent Deck sends no telemetry of its own, no analytics, and loads nothing from a CDN. Every asset the panel renders is local, enforced by a strict Content-Security-Policy. The only socket it listens on is an HTTP listener bound to 127.0.0.1, which is how the hooks reach it — and, if you turn it on, Claude Code's own telemetry (see Claude Code telemetry) — and non-loopback requests are dropped. The only connection it makes is a second VS Code window reaching that same listener on 127.0.0.1 (see Several windows, one port). The OpenCode side opens no socket at all, and Codex's hooks arrive on that same one listener — there is no second port for a second engine. Every socket it opens is on 127.0.0.1.
  • Reasoning and thinking content is never displayed. It is dropped where the data is read, before anything reaches the panel, in all three engines — including a Codex spawn's encrypted task description, which is never decoded. Tool payloads are truncated with an explicit marker.
  • Secret-bearing storage is never opened. On the OpenCode side this is enumerated by name rather than summarised - see Also observes OpenCode; the Codex side is enumerated the same way, in Also observes Codex.

The single qualification to "read-only" — what a read of OpenCode's store touches beside it — is measured in SECURITY.md §2.

What it never does

  • Write to agent settings, transcripts, or databases.
  • Launch, proxy, steer, or configure an agent.
  • Keep session content, or ship a price table.
  • Send data to a network service.

The live deck lives in memory and is discarded when the window closes. The one thing written to disk is the stats history — derived numbers, never session content — kept in VS Code's own storage for this extension; Stats says exactly what is in it and how to turn it off.

Also observes OpenCode

Since v0.5.0, OpenCode sessions appear in the same deck as Claude Code ones. Each cell carries a glyph saying which engine wrote it - OC - and the engine chips filter the deck to one engine or show them all. Nothing is configured: if OpenCode is installed, its sessions are there; if it is not, the deck says nothing about it, because an absent data directory is not an error and not a warning.

Four tables are never read, and this is by name rather than by filter: account, control_account, credential and session_share. Those are the tables whose schema carries access tokens, refresh tokens and share secrets. They are not queried, and they are stripped from every test corpus in the project.

One file is read — the session database under OpenCode's data directory — and nothing else beside it. There is no port, no opencode serve, and no hostname resolved.

Compatibility, same posture as the Claude Code side. The anchor is 1.18.22 — the release whose captured database proved the schema. Major must match, minor may be one step either way (1.17.x to 1.19.x), and the patch component is not compared at all, so a self-update from 1.18.22 to a later patch changes nothing. What refuses a session is the schema: if the tables and columns actually read are not what the corpus pinned, that session renders unsupported rather than a half-built tree. A database holding sessions written by several versions is normal, and the window is applied per session, not to the file.

One thing it does not do yet. An OpenCode session's burn is present and counts the whole prompt, cached tokens included; its context figure reads as an em dash, because that number is a level rather than a total and Agent Deck does not yet read the per-step rows that carry it — so an honest absence is shown rather than a wrong one, never a 0.

Also observes Codex

Since v0.6.0, Codex sessions appear in the same deck as Claude Code and OpenCode ones. Each cell carries a glyph saying which engine wrote it - CX - and the engine chips, labelled Claude Code, OpenCode and Codex, filter to one or show them all. Nothing is configured: if Codex has written sessions under its data root, they are there; if it has not, the deck says nothing about it, because an absent data root is not an error and not a warning.

What is read is the transcripts, and nothing beside them. They live in $CODEX_HOME if you set that variable and in ~/.codex if you do not, and it is checked each time rather than remembered.

Five things under that root are never opened, and this is by name rather than by filter, the same treatment the OpenCode tables get: the credential file auth.json, the sandbox-secret directory .sandbox-secrets/, the two machine identifiers installation_id and cap_sid, the network-fetched models_cache.json, and every local database Codex keeps there. The name is judged before any path is joined or opened, so there is no moment at which one of them has been handed to the filesystem. SECURITY.md enumerates the list.

No App Server, no socket to Codex; secret-bearing files are never opened.

No socket to Codex. No App Server, no app-server proxy, no second port. Codex ships an App Server; Agent Deck never connects to it, and that is a boundary this product keeps rather than a feature it has not got round to. Codex hooks POST to the same loopback listener Claude Code's do — one socket for the whole extension. Neither hooks.json nor config.toml is opened by this extension either: those are Codex's own files, and yours.

Compatibility, same posture as the other two engines. The anchor is 0.151.0-alpha.7.2 — the release whose captured transcripts proved the structure, taken from the corpus's own session_meta.payload.cli_version and never from what a binary reports about itself. Major must match, minor may be one step either way (0.150.x to 0.152.x), and neither the patch component nor the prerelease tag is compared at all. What refuses a session is the structure: if the records actually read are not what the corpus pinned, that session renders unsupported rather than a half-built tree. The anchor moves one way only — by harvesting a corpus from a new release — and moving it cannot make a version work, because the parts it names are the parts nothing compares. Major 0; minor ±1. Patch and prerelease tags are not compared.

One thing Codex gives that the others do not. Its transcripts state the model's context window, so a Codex session's window figure is a real number read from the session. It is stated in two places and one of them can be empty on a turn that ended before any usage was recorded, so the figure comes from whichever of them the session actually carries, and reads as an em dash only when neither does.

One thing it needs that the others do not. Codex's hook block is a separate paste from Claude Code's, in a different file, with a trust step of its own — see Install the Codex hook. Without it a Codex session still renders in full, because the tree, the tool calls and the numbers all come from the transcript; what degrades is liveness, which falls back to file modification times.

Requirements

  • VS Code ^1.134.0
  • Node >=22.22.2 on your PATH — the hook block below is a node -e one-liner, so your Node is what runs it
  • Claude Code on the 2.x line, within one minor of the anchor (see below). Patch releases are read as they come.
  • OpenCode — optional, and there is nothing to install or configure if you do not use it. The version window is in Also observes OpenCode.
  • Codex — optional in the same way, with one difference: liveness needs its own hook block pasted and trusted. The version window is in Also observes Codex and the paste is in Install the Codex hook.

Install

Install from the VS Code Marketplace - open the Extensions view and search for Agent Deck, or run:

code --install-extension nvitlam.agent-deck

Where to find it: the Agent Deck icon in the activity bar. It opens a sidebar with a strip of three tabs — Menu · View · Tweaks — one open at a time. The same three are on the view's title menu.

One window. Deck, Statistics, Insights and About are four surfaces of the one Agent Deck panel, and the Menu switches between them in place — nothing opens a second panel.

Everything is in that sidebar or that menu. The panel is content only — no bars, no buttons, no chips, with the exceptions noted below: Statistics' tabs, and the tiles on Insights and About. You pan, drag, zoom and select; everything else is a sidebar entry, a menu entry or a keyboard shortcut.

Menu

  • Open Deck — the session deck, in the first editor group. It comes back to the deck from wherever you are, and keeps the renderer you chose.
  • Open Statistics — the same panel, on its Stats view, always on the Files tab.
  • Open Insights — the same panel, on its Insights surface. The sidebar shows beside it which state that surface is in: Facts only, or the name of the Insights provider that is registered, with the provider's status line under it when it states one.
  • Pick Agent — under Open Insights, only while the registered provider offers it: asks the provider to let you choose the agent CLI it sends to.
  • Show Payload — likewise: asks the provider to show the payload it would send, for review.
  • Clear History — likewise: asks the provider to clear its stored reports. Agent Deck asks nothing first; a confirmation, if there is one, is the provider's.
  • Show Diagnostics — the Agent Deck output channel.
  • Settings — VS Code's settings, filtered to Agent Deck.
  • Clear Stats History — removes the local stats history, after a confirmation.
  • About — the same panel, on a page with what this is and where to find it.

View — five collapsible groups, each showing what it is set to and folding up again once you choose: Renderer (Canvas or List), Sessions and Engines (the two filters), Layout and Sort. Inspector appears under them while a tool-call drawer is open, with the drawer's status, order and tool filters. Reset view acts on whichever surface you are on.

Tweaks — three settings as checkboxes, each with a line saying what it does: follow new sessions, open the drawer on entering a session, open the drawer expanded. Ticking one writes that setting; the sidebar keeps no value of its own and shows whatever the settings say. The deck's opening order is agentDeck.defaultOrdering in Settings — the deck's own order is View ▸ Sort.

Statistics keeps its five tabs — Files, Tools, Loops & churn, Tokens, Trends. Insights and About carry tiles, and a tile that opens a web page asks first. With an Insights provider registered, Insights carries its report list — click a report to preview it, tick reports for a batch — the preview's HTML, Markdown and Copy export actions, and Export ticked. Nothing else on the panel is pressed.

The keyboard shortcuts are unchanged, and they work while the deck panel has focus: a c o x for the engines, 1 2 3 for the layout, l r e for the sort. Escape walks back out of a session, and k collapses the tree.

The Agent Deck sidebar in the activity bar — a screenshot of an earlier release, to be retaken

The sidebar's settings — a screenshot of an earlier release's Tweaks tab, to be retaken

Every entry is also in the Command Palette, under Agent Deck:. Your sessions appear on their own — there is nothing to point it at and nothing to switch on.

Several VS Code windows work as they are. Each window's deck stays live, sharing the one hook listener on the machine, with nothing to configure — see Several windows, one port.

Then install the hook block below. Optional. Without it Agent Deck still shows the tree, but it cannot tell you what is running right now — liveness is inferred from file times and the panel says so.

Install the hook (one manual paste)

This block is Claude Code's. Codex has its own, further down; OpenCode needs none.

Content and the tree render from the session files alone. The hook is what makes liveness live — which agent is running right now, which tool call is in flight.

Agent Deck never installs this for you and never writes either settings file. Read-only includes your configuration: you paste it, you own it.

Paste the "hooks" key below into your user-level ~/.claude/settings.json — one paste, and every project on this machine is covered, in every window.

If you would rather scope it to one project, the same block works in that project's own .claude/settings.local.json instead; that is what this repository does, and it leaves ~/.claude untouched.

Both files are JSON objects. Merge the "hooks" key into whatever is already there rather than replacing the file.

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\"",
            "timeout": 5
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\"",
            "timeout": 5
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\"",
            "timeout": 5
          }
        ]
      }
    ],
    "SubagentStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\"",
            "timeout": 5
          }
        ]
      }
    ],
    "SubagentStop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\"",
            "timeout": 5
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\"",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Notes on that block, each of them measured rather than assumed:

  • It is node -e, not curl, and that is not a style choice. This command runs inside your real Claude Code session on every tool call. Against a closed loopback port — which is what it finds whenever Agent Deck is not running — node takes ECONNREFUSED and exits 0 immediately, while curl.exe burns its full connect timeout and stalls your session that long every single time. Simplifying it to curl costs you roughly an order of magnitude, forever, on the common path. Measured in SECURITY.md §5.
  • The port must match agentDeck.port. The block names 47821 literally, which is that setting's default. If you change one, change the other: Agent Deck never silently picks a different port, because the block you pasted has no way of being told. A collision with a program that is not Agent Deck is reported as an error; a collision with a second Agent Deck window is not a collision at all — see Several windows, one port.
  • No Claude Code restart is needed. Hook settings are re-read per invocation — registering a new event and seeing it arrive without a restart was measured on 2.1.234.
  • The POST is unconditional. With nothing listening it is refused and nothing happens. A quiet listener is not evidence that hooks stopped firing.
  • Six events are registered: SessionStart, PreToolUse, PostToolUse, SubagentStart, SubagentStop, Stop. Registering fewer still works — liveness degrades rather than fails, and falls back to transcript modification times with a banner — but the panel gets blunter.

Claude Code telemetry (optional)

What it adds: a cost figure for each Claude Code session this window sees start, estimated by Claude Code itself, in the Stats view's Tokens part with the label estimated by Claude Code. It also adds the duration of a tool call where the session's own records state none, in that session's stats record — the local history and the extension API, and in the Stats view's Tools part, as each tool's longest call and total duration.

The Stats view's Tokens part: one Claude Code session's cost, estimated by Claude Code, and its tokens per agent

Claude Code can export OpenTelemetry — metrics, logs and traces — to an address you give it. Agent Deck's hook listener accepts that export on the same port as the hooks: 127.0.0.1 at agentDeck.port, 47821 by default, on the paths /v1/metrics, /v1/logs and /v1/traces, as OTLP over HTTP in JSON. Agent Deck never sets this up and never writes Claude Code's settings. You paste it; you own it.

Two steps:

  1. Merge the "env" key below into your user-level ~/.claude/settings.json. If the file already has an "env" object, add these keys to it rather than replacing it. The endpoint names the port in agentDeck.port: change one and you change the other.
  2. Turn on agentDeck.telemetry.enabled. Until you do, the three paths answer 403 with a body naming that setting, and no body is parsed.
{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA": "1",
    "OTEL_METRICS_EXPORTER": "otlp",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_TRACES_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:47821",
    "OTEL_METRICS_INCLUDE_SESSION_ID": "true",
    "OTEL_METRIC_EXPORT_INTERVAL": "5000",
    "OTEL_LOGS_EXPORT_INTERVAL": "2000"
  }
}
  • The setting is machine-scoped. Every VS Code window on the machine reads the same value, and the window holding the port answers for all of them.
  • The four content flags stay unset: OTEL_LOG_USER_PROMPTS, OTEL_LOG_ASSISTANT_RESPONSES, OTEL_LOG_TOOL_DETAILS and OTEL_LOG_RAW_API_BODIES. Each one puts real text on the wire. With all four unset, Claude Code sends the literal <REDACTED> in place of prompts and responses, and Agent Deck drops those fields where the body is parsed either way.
  • The cost is estimated by Claude Code, not an engine report. It is Claude Code's own cost metric, which Claude Code exports as increments, summed per session over the increments this window has received. It is shown only for a session whose start this window received — the claude_code.session.count point Claude Code sends once when a session starts — so for a session already under way when this window opened, when the setting was turned on, or across a window reload, it is not shown and that session's stats record names F9:telemetry-partial. Where an engine's session records state a cost, that figure is shown instead; where neither an engine cost nor a telemetry cost from the session's start exists, a cost from your own prices (agentDeck.pricing) is.
  • What is dropped where the body is parsed: the five account attributes Claude Code attaches to every record — user.email, user.id, user.account_id, user.account_uuid and organization.id — and every attribute Agent Deck does not read. What is kept is the session id, the tool-call id, the tool name, the duration and the cost.
  • Telemetry never touches the liveness or stall clock. It does not make a session live and does not clear a stall, and it never makes Agent Deck record a session this window has not seen working. For a session it is already recording, a cost change — or a tool duration filled from a span — may produce a newer stored record and may delay the idle write, like any change to the record: the agentDeck.stats.idleFlushMs countdown restarts on each one. A session id that appears only in telemetry adds nothing to the deck.
  • Rows about sessions this window does not show yet. The exporter is machine-wide, so rows about other sessions arrive too. A session's start and its cost, arriving before this window shows the session, are kept for up to 256 such sessions and joined once it appears; a tool span waits one update for its session and is joined if that update shows it.
  • unmatched and foreign on the Agent Deck output channel, for this window. A tool span is judged at the next update after it arrives. If that update shows its session and the tool call it names, it joins and is not counted. If it shows the session and not the call, the span is counted unmatched, and one line records its session.id and tool_use_id — nothing else from the span. If the window does not hold the session at all, the span is counted foreign and writes no line. A session's start or cost held for a session not shown yet counts as foreign if 256 newer sessions push its slot out. A row that arrives early and joins a moment later is not counted. The line ends its telemetry figures with otel.unmatched-scope=(this window).
  • The answers: 200 accepted · 400 not an OTLP JSON body · 403 the setting is off · 405 not a POST · 413 over 512 KiB · 415 not JSON. None of them asks the exporter to retry.

Install the Codex hook (one manual paste)

Codex keeps its hooks in its own file and gates them behind its own trust step, so this is a second paste rather than a variant of the one above. Both engines POST to the same listener on the same port: there is no second socket, and nothing else to turn on.

Paste the "hooks" key below into ~/.codex/hooks.json — the user-level file, and the only place this is offered. Repo-local hook discovery has been reported broken on some Codex releases, and a paste that looks installed and never fires is worse than one you had to put somewhere central. If you set $CODEX_HOME, that is where the file goes instead — the variable moves every Codex file, this one included.

That file is a JSON object. Merge the "hooks" key into whatever is already there rather than replacing the file; if it does not exist yet, create it with exactly what is below.

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\"",
            "commandWindows": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\""
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\"",
            "commandWindows": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\""
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\"",
            "commandWindows": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\""
          }
        ]
      }
    ],
    "SubagentStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\"",
            "commandWindows": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\""
          }
        ]
      }
    ],
    "SubagentStop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\"",
            "commandWindows": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\""
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\"",
            "commandWindows": "node -e \"let b='';process.stdin.setEncoding('utf8');process.stdin.on('error',()=>process.exit(0));process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{const r=require('http').request({host:'127.0.0.1',port:47821,path:'/event',method:'POST',headers:{'content-type':'application/json','content-length':Buffer.byteLength(b),connection:'close'}},s=>{s.resume();s.on('end',()=>process.exit(0))});r.on('error',()=>process.exit(0));r.setTimeout(1000,()=>{r.destroy();process.exit(0)});r.end(b)});setTimeout(()=>process.exit(0),2000)\""
          }
        ]
      }
    ]
  }
}

Then, in this order — and the order is the point, because every step after a missed one looks exactly like "the extension does not work":

  1. Restart Codex. It reads this file at startup, so nothing you paste arrives in a session that was already running. This is the opposite of Claude Code, which re-reads its own hook settings per invocation.
  2. Trust the hook when Codex asks. Codex will not run a hook command it has not been told to trust. That prompt is Codex's own and it is the only step Agent Deck cannot do anything about, because Agent Deck never writes either of the files involved.
  3. Expect six of those, not one. Six events sharing one identical command produce six distinct trust entries, one per event. Trusting one of them arms one event.
  4. Re-trust after any edit. Editing hooks.json invalidates the trust entry for the events you touched, and those hooks then stop firing silently — no error, no warning, just a deck that has gone quiet. If liveness stops after you edit that file, this is why.

Notes on that block, each of them measured rather than assumed:

  • Both command and commandWindows are given, carrying the same one-liner. This is a byte-for-byte copy of the block that produced this project's captured Codex hook corpus, both keys included; a hand-trimmed version of it is a version nothing here has evidence about.
  • It is node -e, not curl, for the reason the Claude Code block gives above. Against a closed loopback port — what the hook finds whenever Agent Deck is not running — node takes ECONNREFUSED and exits in well under a fifth of a second, while curl.exe burns its full connect timeout on every tool call of every session. SECURITY.md §5 carries both engines' numbers.
  • The port must match agentDeck.port, which is the same 47821 the Claude Code block names, because it is the same listener. Change the setting and you change both blocks.
  • Six events are registered: SessionStart, PreToolUse, PostToolUse, SubagentStart, SubagentStop, Stop. Registering fewer still works — liveness degrades to transcript modification times rather than failing — and it is also six trust prompts rather than seven, which is why the list is exactly this long.
  • Agent Deck reads neither hooks.json nor config.toml, and writes neither. You paste it; you own it. What reaches the extension is what your Codex sends to the loopback listener, and nothing else.

Several windows, one port

Open a second VS Code window and both decks stay live. You do not configure anything, and there is still exactly one socket on the machine.

That is worth spelling out because the obvious arrangement does not work. The port is fixed — the hook block you pasted names 47821 literally, and there is nowhere to publish a different number without writing a file into Claude Code's own configuration, which Agent Deck never does. So a per-window port is not available, and before v0.7.0 the second window simply failed to bind it and showed you a deck with no liveness at all.

What happens instead:

  • The first window to start binds the port and becomes the leader. Nothing about it changes: it serves the hook route exactly as it always has.
  • A later window finds the port taken, asks what is holding it, and attaches to the leader's event stream when the answer is another Agent Deck. It holds no socket of its own.
  • Each window still reads its own workspace's transcripts. Only the hook stream is shared, and the windows attached to the leader keep only the events belonging to a session they are following or to a folder they have open. Your other project's activity does not appear on this project's deck: what is on a deck comes from the transcripts that window reads, not from the hook stream.
  • Close the leader's window and the others race for the port. Whoever wins serves the rest. There is no election, no lock file and no coordinator: the operating system decides, because exactly one process can bind a port. The changeover takes a fraction of a second and the events that arrive during it are lost rather than queued — the stream keeps no buffer, by design.
  • If the port is held by something that is not Agent Deck, you get the same error you always did, naming the port. Agent Deck will not pick a different one for you.

None of this leaves your machine. The stream is served on 127.0.0.1 and read from 127.0.0.1, on the port you configured, and it carries only what the hooks already POST — after the same redaction the panel applies, so no reasoning content and no oversized payload crosses it. Loopback means same-user trust here exactly as it does for the hook listener itself: there is no token and no authentication, because a process running as you could read the hook payloads anyway.

Which sessions does a window show?

The sessions belonging to the folders that window has open. A window is scoped to its workspace, not to the machine. What reaches its deck comes from the transcripts that window reads, and it reads only the ones its open folders account for — another project's run belongs to that project's window.

The scope is a project key: the folder's path with :, \, / and spaces each folded to -, which is how Claude Code names the directory it keeps a project's transcripts in under ~/.claude/projects. All three engines are matched against that one key, and case is dropped on both sides, because a Windows drive letter is spelled both ways in real data: a path beginning c: and a path beginning C: name one project.

  • Claude Code — the first open folder's key. Sessions are discovered under that one project directory and no other is read, so in a multi-root workspace the second folder onward contributes no Claude Code sessions.
  • OpenCode — every open folder. Each session records the worktree it ran in; that path is folded to a key and compared with the key of each folder.
  • Codex — every open folder. Each transcript declares the directory it ran in, compared the same way. A transcript that declares no directory matches no folder.

Two answers are neither a match nor an error:

  • No folder open. Nothing is observed and no panel opens — the command reports Agent Deck: open a folder to see its sessions.
  • A folder Claude Code has never run in. There is no project directory to read yet, so the Claude Code half stays off — no watcher and no timer for it — while OpenCode and Codex are read as usual. The hook listener is bound anyway, and the first Claude Code hook event whose working directory is this folder starts the Claude Code half, with no reload. The panel opens either way.

A refused session is shown wherever it ran. An OpenCode or Codex session whose schema Agent Deck refuses appears as an unsupported card whatever folder produced it, because a refusal nobody can see is not a refusal. Every other session is matched to the folders above.

Stats

What each session touched, repeated and spent — as numbers. Menu ▸ Open Statistics in the sidebar, or Agent Deck: Open Statistics in the Command Palette, switches the panel to its Stats view, in five parts: Files (every file a session touched, with its reads, edits, writes and errors), Tools (every tool a session called, with its calls, errors, longest call and total duration), Loops & churn (every call in a chain is a link back to the tree), Tokens (per agent: prompt, output, cache ratio and context fill where the engine states them, context-churn and compaction markers on a per-turn strip, stalls, and cost with its source beside it, and the session's own timings — wall time, time to the first tool call, the longest gap between calls, tokens and calls a minute — and, beside the silent subagents, the subagents whose spawning call has no result) and Trends (one point per stored session, with tokens a minute drawn per engine). The engine chips narrow every part exactly as they narrow the deck. A session Agent Deck could not read in full is counted in the footer with its reason, and appears in no table.

The Stats view's Tokens part for one Claude Code session: wall time, time to the first tool, longest gap, tokens and calls a minute, and cost an hour

Everything here is a count, a ratio or a token figure taken from the structure of a session. None of it reads message text, tool payloads or reasoning, and none of it says why a number is what it is.

The vocabulary

Seven things have names. Each is a rule over a session's tool calls and token counts.

  • Re-read loop — one agent makes the same call, with an identical input, three or more times (the measurement parameter LOOP_MIN, 3), and the tool is one that reads a file. The same repeat with any other tool is listed as a loop.
  • Churn chain — one agent edits or writes a file, a later call by the same agent ends in an error, and the agent edits or writes that file again. The chain lists every call between the two writes, how many of them failed, and how many of those failures named that same file.
  • Context churn — a turn in which the tokens written to the prompt cache rose by 5,000 or more over the previous turn (the measurement parameter SPIKE_TOKENS), with the time between the two turns where the engine states both. Claude Code only: it is the one engine the threshold was measured on.
  • Silent subagent — a subagent that was spawned and made no tool call at all.
  • Compaction — the engine's own record that it compacted the conversation, with the prompt size before and after where the engine states them.
  • Stall — a tool call still running while the session has been silent for longer than agentDeck.livenessThresholdMs (default 120 seconds). It measures silence, not duration: a long call that is still reporting activity is not stalled. It clears the moment anything arrives.
  • Waiting on you — a stall on AskUserQuestion or ExitPlanMode, the two tools that stop to ask you something. Same rule, same amber; only the words change.

What each engine can supply

The three engines do not write down the same things, so a fact is sometimes not there at all. Where it is missing the view shows an em dash — never a zero, and never a figure borrowed from another fact.

Fact Claude Code OpenCode Codex
Files read, edited and written yes yes no
Tool calls and errors, per tool yes yes calls only, no error status
Loops yes yes yes
Churn chains yes yes no
Prompt and output tokens, per agent yes yes yes
Cache ratio where the session states a cache split where the session states a cache split no
Context churn yes no no
Silent subagents yes yes yes
Cost estimated by Claude Code, with its telemetry on; or from your own prices where OpenCode reports one, or from your own prices no
Context fill no no yes
Compactions yes yes no
Stalls yes yes yes

A no means the engine's own records do not carry what the fact is built from. Context fill needs the model's context window, which only a Codex transcript states; a cost from your own prices needs a per-turn token series, which a Codex transcript does not carry.

The five settings

All five are in the Settings table with their defaults.

  • agentDeck.stats.enabled — keep the local history. Off, Agent Deck writes no file and creates no directory; the Files, Loops and Tokens parts still show this window's sessions.
  • agentDeck.stats.retentionDays — how many days of history to keep.
  • agentDeck.stats.idleFlushMs — how long a session may stay unchanged before its record is written anyway.
  • agentDeck.pricing — your own prices, for a cost figure where the engine reports none. Below.
  • agentDeck.canvas.autoFit — re-fit the session canvas on every change to its geometry. Not a statistics setting; it arrived in the same release.

Your own prices. Agent Deck ships no price table and never guesses a price. A cost appears only where the engine reports one, where Claude Code's own telemetry estimates one from the session's start (see Claude Code telemetry), or where you have entered prices for the model a session ran — in that order of precedence. Prices are in USD per million tokens, keyed by the model id exactly as the session writes it — the Tokens part lists every id it has seen, so you can copy it. The figures below show the shape and are not a price list:

{
  "agentDeck.pricing": {
    "claude-sonnet-4-5": { "prompt": 3, "cacheRead": 0.3, "cacheWrite": 3.75, "output": 15 }
  }
}

Worked through: a turn of 2 fresh prompt tokens, 13,390 tokens written to the cache, 28,807 read from it and 1,000 output tokens costs (2 × 3 + 13,390 × 3.75 + 28,807 × 0.3 + 1,000 × 15) ÷ 1,000,000 = $0.0739. A cost worked out this way is labelled as estimated from your prices. An entry that is not four non-negative numbers is ignored and named on the Agent Deck output channel.

A subscription plan yields no per-token cost. A flat-rate plan has no price per token to enter, so there is nothing to put in this setting for it, and its sessions show no cost figure.

Clearing the history

Clear Stats History — in the sidebar's Menu tab, or Agent Deck: Clear Stats History in the Command Palette — deletes the whole history after a modal confirmation, and works whether or not agentDeck.stats.enabled is on. There is one history per machine, so it is cleared for every window at once; another window that is already open keeps showing what it had read until it next writes a record or is reloaded. A session still running refills the history as it goes.

Where it lives, and what leaves the machine

Records from 0.7.x are read; time facts are absent for them. Nothing on disk is rewritten. Stored history is what Trends draws: such a record is a point in every Trends series except tokens per minute, and the footer counts it as F14:absent. A stored record written before a churn chain carried its same-file failure count, or before a context-churn turn carried its time since the turn before, names the gap as fileErrors:absent or gapBeforeMs:absent.

Nothing leaves the machine. No upload, no sync, and no telemetry sent. The history is kept in VS Code's global storage for this extension, as one JSON Lines file per week — not under ~/.claude, not under ~/.codex, not in OpenCode's directories, and not in your workspace. A record holds counts, token figures and identifiers the engines wrote — session ids, agent ids, tool names, file paths, model ids — and never message text, tool payloads or reasoning. Turn it off with agentDeck.stats.enabled; remove it with Clear Stats History.

Two limits, stated plainly

  • Without the hook block, a Claude Code session's record is written after an hour without change, not when it ends. The hook stream is how Agent Deck learns that a Claude Code session has ended; without it the idle rule (agentDeck.stats.idleFlushMs, one hour by default) is what writes the record.
  • A session that finishes while no VS Code window is open is not recorded. Agent Deck records what it observes while a window is running. It never reads old transcripts back into the history.

What a record carries about agents and skills

Each subagent carries the agent type Claude Code recorded for it. The description beside it is not carried: it is prose written by whoever spawned the agent, and Agent Deck keeps prose out of these records.

Each session carries the skills it invoked, by name and by position in that session’s calls. The arguments passed to a skill are not carried, for the same reason. Which later tool calls a skill produced is not something Claude Code writes down, so Agent Deck does not claim it.

A file path over 1024 characters, or an agent type or skill name over 64, is left out rather than shortened — a shortened path is still a path — and the record names what was left out. The session keeps every other number it has.

For extension authors

vscode.extensions.getExtension('nvitlam.agent-deck')?.exports is Agent Deck's extension API. apiVersion is 2, and version 2 only adds to version 1: getLiveStats(), getStoredStats({ sinceMs, limit }) and the event onDidUpdateStats, which fires for every record written and, while a session changes, at most once every two seconds for that session. It hands out these records and nothing else — never a session's tree and never a preview.

Version 2 adds registerInsightsProvider: registerInsightsProvider(provider) returns a disposable. A provider is:

{
  providerVersion: 1;
  about: { name: string; version: string; status?: string }; // status: one line, e.g. "licensed until 2027-09-23"
  getLatest(): FindingSetView | null;               // required; not called since 0.9.0's report list
  listRuns(): RunSummary[];
  getRun(runId: string): FindingSetView | null;     // the run the user selects or exports
  run?(): Promise<void>;                            // optional; not called — no Run action
  getRawOutput?(runId: string): string | null;      // optional
  pickAgent?(): Promise<void>;                      // optional — the sidebar's Pick Agent
  showPayload?(): Promise<void>;                    // optional — the sidebar's Show Payload
  clearHistory?(): Promise<void>;                   // optional — the sidebar's Clear History
  investigate?(runId: string): Promise<void>;       // optional — the preview's Investigate Report
  onDidChange: Event<void>;
}

FindingSetView and RunSummary are plain JSON types defined by Agent Deck and exported from its API module with every type they use:

FindingSetView {
  runId: string;
  createdAt: number;
  agent: { kind: 'claude' | 'codex'; version: string };
  window: { sessions: number; excluded: number; sinceMs: number };
  usage: { prompt: number; output: number; costUsd?: number } | null;
  resolvedKinds: string[];      // kinds in the previous set and absent now
  findings: FindingView[];      // empty unless state is 'ok'
  rejected: number;
  state: 'ok' | 'empty' | 'refused';
  refusal?: { step: string; reason: string }; // present exactly when refused
}
FindingView {
  id: string;
  kind: 're-read-loop' | 'churn-chain' | 'context-churn' | 'stall'
      | 'silent-subagent' | 'compaction' | 'cache-miss' | 'other';
  confidence: 'low' | 'medium' | 'high';
  action: { lead: string; detail: string }; // lead: at most 15 words, one line; detail may be ''
  cause: string;
  evidence: { label: string; sessionId: string; statsKey: string; value: number | string }[];
  sinceLastRun: 'new' | 'still' | 'resolved' | null;
}
RunSummary { runId: string; createdAt: number; state: 'ok' | 'empty' | 'refused'; findings: number; agentKind: 'claude' | 'codex' }

providerVersion is 1, and a provider fires onDidChange whenever what listRuns or getRun would return has moved. The Insights surface lists listRuns() newest first — Agent Deck sorts it — and previews the run you select through getRun(runId); a set whose own runId is not the one asked for is dropped. about.status, when present, is one line of at most 64 characters shown under the provider's name; one that fails the check is left out. getLatest is still required and run is optional, so a provider written for the earlier contract registers unchanged; Agent Deck calls neither. The preview shows each finding's action lead first, its kind, confidence and "since last run" as words (never a score), the detail behind an expand (no expand when the detail is empty: a one-sentence action), the cause, then each piece of evidence under its label. A number whose statsKey names milliseconds (its last part carries Ms as a word: longestGapMs, durationMsSum, durationMsMax) is printed whole with a duration beside it — 28,100,113 ms · 7 h 48 m. A cost (costUsd, costPerHourUsd) and a ratio (cacheRatio, contextFill) are shown to two places and a per-minute rate as a whole number, with the exact value in the tooltip. Every export prints the same, with the exact value as the HTML title and beside the value in Markdown and plain text. When the set names kinds that were in the previous set and are absent now, one line says No longer reported: and names them; each must be one of the eight kinds, said once, and not a kind the set still lists. Above them it states the run: when, which agent CLI and version, the window, and the run's own usage — marked estimated by Claude Code when the agent was Claude Code. A refused run shows the step and the reason, and a Show raw output action that asks getRawOutput for the selected run's runId and opens what comes back as an untitled plain-text document. When the provider has no getRawOutput, the refused run says No raw output for this run. instead. When the provider has investigate, the preview's header shows Investigate Report beside Export; pressing it calls investigate with the selected run's runId and nothing else. Agent Deck builds no prompt, starts no process and does not know what the provider does next. Without investigate there is no button. If the provider throws or its promise rejects, the Agent Deck output channel gets one line and a message names the action. One provider at a time — a second registration throws, naming both — and disposing the registration returns the surface to its free state.

What a provider hands over is data: provider data is plain JSON, checked field by field, and never executed. Agent Deck reads what the provider returns as the objects' own data properties, never through a getter; every enum is checked against its list; every id, version and stats key must match a fixed shape; lists are capped (64 findings, 16 evidence items each, 50 runs); a run id, a finding id, or one stats key of one session that repeats is refused. Every text is length-capped and checked: a name (an evidence label, a refusal's step) at most 64 characters, a path at most 1,024, free text (an action, a cause, a refusal's reason) at most 2,000 — the first two are the stats history's own caps — with no character from the Unicode categories Cc and Cf (free text may carry a tab and line breaks, LF or CRLF): that excludes zero-width spaces and joiners, bidirectional marks and overrides, the soft hyphen, the byte-order mark and tag characters; nor a line or paragraph separator, nor a lone surrogate. Evidence may be text only on a stats-record field the history itself stores as text: a file path at most 1,024 characters (the history's cap), an agent type or skill name at most 64 (likewise), a project slug at most 1,024 and any other such field at most 64. A set whose state and findings disagree is refused whole. A value that fails is dropped and counted, never shortened, and the surface says how many were dropped — an export of that report says so too. (A status line that fails is simply left out: it is not part of any report.) Raw output over 1,048,576 characters is not opened at all. Agent Deck calls listRuns, getRun for the run you select or export, getRawOutput only when you ask for a refused run's raw output, pickAgent, showPayload or clearHistory only when you press that sidebar row, investigate only when you press Investigate Report, and subscribes once through onDidChange; it calls nothing else.

Insights

Menu ▸ Open Insights — a surface of the one panel. Nothing Agent Deck does depends on Insights, and no feature of Agent Deck moves behind it.

Free — no Insights provider registered. The facts the stats history already holds for the last 7 days, as tiles, each naming the record field it was counted from: sessions by engine, compactions, long-idle resumes (sessions whose longest gap between calls is at least your agentDeck.livenessThresholdMs, 120 seconds by default — the tile names the threshold in milliseconds with its duration beside it, 120,000 ms · 2 m), re-read loops, failed tool calls, stalls, silent subagents, prompt and output tokens, and the cost the engines reported themselves (cost estimated by Claude Code or from your prices is not added in). A session read only in part is not counted, and the surface says how many were left out. Under the tiles, one line of fact: Deliberate failures (test-driven breakage) and accidental ones are indistinguishable in this data. Below them, one of three examples, labelled "Example, based on a real run", with made-up ids; it changes each time you come back. And one tile, Get Agent Deck Insights, which asks before it opens https://agent-deck.app/insights.html in your browser — the Insights page, with what it does, what it never does, and the plans.

Term Price Plans
1 month $10 Pay once for one month, or subscribe monthly.
6 months $50 Pay once for six months, or subscribe every six months.
1 year $100 Pay once for a year, or subscribe yearly.

Every plan is the same product; a subscription renews your key automatically, a one-time purchase does not.

Agent Deck itself is unaffected: no feature moves behind a plan, and nothing it already does depends on Insights being installed.

Never

  • reading transcripts
  • reading files
  • network calls from the extension
  • writing under any engine's data directory

With a provider registered. On the left, the provider's reports — each with its date, how many findings it has and the agent CLI it used, newest first, a refused one marked refused. On the right, Select a report to preview / download. until you click one; then that report, exactly as the extension API section describes it. Click the selected report again and the preview goes back to that prompt. The same fact tiles as the free state sit below both, with the same line of fact under them. There is no Run button here: a run is started from Insights' own window. If the provider goes away, the sidebar reads Facts only again, its rows under Open Insights go, and this surface shows the free view; the deck and Statistics are left as they were. The Agent Deck output channel writes one line each time a provider registers, deregisters (with the reason) or is refused.

Export. The preview's header carries HTML, Markdown and Copy — and Investigate Report beside them when the provider offers it, which hands the selected report to the provider. HTML is one self-contained page — its own stylesheet, no script, no image, nothing it loads, and a content security policy that forbids loading anything; Markdown escapes the provider's text so none of it becomes a link, an image or HTML; Copy puts plain text on the clipboard. HTML and Markdown ask where to save with the editor's own save dialog. Tick reports in the list and Export ticked (n) asks the format, then a folder, and writes one file per report there, never replacing a file already in it (a name that is taken gains -2, -3…). Every export is built from what the provider returns for that run at that moment, checked as above. Agent Deck refuses to write an export into a directory it only reads — ~/.claude, the Claude Code projects directory, the Codex directory or OpenCode's data directory — and says so. Exporting makes no network call.

Agent Deck Insights is a separate extension that registers as that provider. Agent Deck has no knowledge of your Insights licence — Insights registers only once it has checked its own licence, and Agent Deck only asks whether a provider is registered. Whether Insights is installed is never consulted, and the sidebar states the same thing whether or not the panel is open.

About

Agent Deck: About in the Command Palette, and About in the sidebar's Menu. A surface of the one panel, in the deck's own look: a short introduction, four tiles — Portfolio, Repository, LinkedIn and Sponsor — and a footer line with the version and the licence (MIT). While no Insights provider is registered a fifth tile, Get Agent Deck Insights, is lit; once one is registered, About names it and its version instead, with its status line when it states one. A tile asks before it opens anything: "Agent Deck will open <host> in your browser", with an Open button. The links open through VS Code — the extension opens no socket for them and makes no network call of its own. SECURITY.md §1 states that and names its proofs.

Claude Code version window

  • Anchor 2.1.246 — the release the committed corpora were captured from. It is a provenance anchor rather than a support claim: it names the release whose structure was proved against real bytes, and it moves only when a new corpus is harvested.
  • Accepted 2.0.x to 2.2.x — major exact, minor +/-1. The patch component is not compared at all. Whatever Claude Code ships next on this line is read.
  • What refuses instead is the structure — a required field missing or wrong-typed, a subagent record without its join key, the subagent directory convention moving. Those are the changes that would make the rendered tree wrong, and they are the ones worth refusing on.
  • Out-of-range, malformed and unreadable versions are still refused: the session renders unsupported, never a partial tree.
  • A session imported from another machine is not supported. Claude Code's --teleport writes the imported history into the local transcript with a version this window does not accept, so the whole session renders unsupported — including the part of it that continued locally. Sessions started on this machine are unaffected.
  • A session whose version changes partway through — Claude Code updating itself while you work — is accepted while every version in it stays in range, and refused as versionChangedMidFile once the drift leaves it.

How the anchor moves, and why it is not a lever. One way only: capture a session from the new release, check the structural assertions against those bytes, commit the corpus, then move the anchor. It is never moved to make a version work, because moving it cannot make anything work — the patch number is not consulted. If a new release breaks the deck, the structural rules are what changed, and those are what need looking at.

What this costs, stated plainly. Reading releases nobody captured means reading releases nobody verified, so a structural change we have not seen can surface as a wrong tree rather than an honest refusal. The alternative was measured, twice: a tolerance counted in patch releases expires, and when it expired on 2026-08-24 every session for every user rendered unsupported. Tightening the string does not buy correctness; it buys a blackout. The structural assertions are where the honesty is kept, and they were not loosened alongside it.

What it does not do

  • No writes to anything it observes. Not to ~/.claude, not to your Claude Code settings, not to session files, not to OpenCode's database or its config, not to Codex's hooks.json or config.toml. The one qualification is stated in full under Trust rather than buried here. Its own stats history is the one file it writes on its own, in its own storage — see Stats; an Insights report you export is written only where you choose, and never into a directory Agent Deck observes.
  • No launching, wrapping or proxying any of the three engines. It observes what is already there.
  • No session replay. Close the window and the live deck is gone. The one thing kept is the stats history — derived numbers, which you can turn off and clear — and it is never read back into a deck.
  • It sends no telemetry, no analytics, and nothing off the machine. Claude Code's own telemetry can be pointed at the loopback listener — that is Claude Code sending to this machine, never Agent Deck sending anywhere.
  • No price table and no cost analytics. A cost figure appears only where an engine reports one, Claude Code's telemetry estimates one, or you have entered your own prices, beside the session it belongs to.
  • No control surface. It cannot start, stop, steer or configure an agent, and it is not going to grow one by accident.
  • No settings for the OpenCode or Codex sides. Each is on when its data directory exists and silent when it does not; there is nothing to turn on. Codex's hook block is the one thing you paste, and it is pasted into Codex, not into Agent Deck.

Settings

Setting What it does
agentDeck.port The loopback port the hook listener binds on 127.0.0.1. Must match the port in the block you pasted.
agentDeck.livenessThresholdMs How long a session may go quiet before it stops counting as live. Set it too low and one long tool call makes a healthy session flap.
agentDeck.previewBytes Ceiling on tool-payload bytes kept per node for previews. Nothing is ever sent off the machine either way.
agentDeck.codex.maxTranscriptBytes Largest Codex transcript Agent Deck will open, in bytes. Default 67108864 (64 MiB). A bigger rollout file is read in part: its first 256 KiB and its last 16 MiB. Its deck card reads "read in part", and the Agent Deck output channel names the file and the bytes read. Codex stores tool output whole and inline, so a long session can reach hundreds of megabytes.
agentDeck.stats.enabled Keep a local history of derived session statistics. Append-only JSON Lines under the extension's own global-storage directory, one file per ISO week — never under ~/.claude, ~/.codex, the OpenCode directories, or your workspace. Off means no file and no directory at all. Nothing is ever sent off the machine.
agentDeck.stats.retentionDays How many days of that history to keep. Default 90. Files are pruned when a record is written, and a weekly file goes once the whole week it covers has aged out. To keep nothing, turn agentDeck.stats.enabled off — the floor here is one day, not zero.
agentDeck.canvas.autoFit Re-fit the session canvas to its content on every event that changes its geometry: a node selected, the drawer opened, expanded or closed, an agent grafted, removed or parked, the panel resized, a session switch, an engine chip, a switch back to the canvas. Default true. A manual pan or zoom persists until the next such event; token counters and status colours never re-fit. Off, the canvas fits once on entry and on Reset view.
agentDeck.stats.idleFlushMs How long a session may go unchanged before its record is written anyway, in milliseconds. Default 3600000 (one hour). A record is normally written when the session ends; this covers the session that never does. Later work is recomputed in full and written as a second record; reads keep the newest per session and nothing on disk is rewritten.
agentDeck.pricing Your own prices per model id, in USD per million tokens — {"<model id>": {"prompt": 3, "cacheRead": 0.3, "cacheWrite": 3.75, "output": 15}}. Used only for sessions for which neither the engine nor Claude Code's telemetry, received from the session's start, states a cost. Agent Deck ships no price table and never guesses one: a model with no entry gets no figure, a malformed entry is ignored and named on the output channel, and anything computed this way is labelled as estimated from your prices.
agentDeck.telemetry.enabled Accept Claude Code's own OpenTelemetry export on the hook listener's /v1/metrics, /v1/logs and /v1/traces paths. Default false: off, those paths answer 403 and no body is parsed. Machine-scoped, so every window on the machine reads the same value. See Claude Code telemetry.
agentDeck.followNewSessions Select a session that appears while the deck is open, so the deck moves to it. Default false: the new session is added in its sort position and the current selection is left alone. This changes what the deck shows and never what is observed. Also in the Tweaks tab of the sidebar, which reads and writes this same value.
agentDeck.openDrawerOnEnter Open a session's tool-call drawer when the session is entered from the deck. Default false: the drawer opens when a tool call is selected. The drawer holds the same calls either way. Also in the Tweaks tab.
agentDeck.drawerExpandedByDefault Open the tool-call drawer at its expanded height rather than its collapsed one. Default false. The drawer can be expanded and collapsed in the panel at either value; this is the height it opens at. Also in the Tweaks tab.
agentDeck.defaultOrdering The order deck cards are placed in when the deck opens. Default live, which puts live sessions first, then idle, degraded, unsupported and ended; recent puts the most recently active first; engine groups the cards by the engine that produced them. The order chosen on the deck itself — View ▸ Sort — applies to that deck and leaves this value alone. It has no sidebar entry: the sidebar's Sort group is the deck's own order, and this is what the deck opens with.

Clearing the history is a command, not a button on the deck: Agent Deck: Clear Stats History in the command palette or the sidebar, behind a modal confirm. It works whether or not agentDeck.stats.enabled is on, so turning the store off and then removing what it already wrote is two steps rather than a dead end.

Development

Build and side-load from a checkout:

npm ci
npm run build
npm run package
code --install-extension dist/agent-deck.vsix

Licence

See LICENSE.

Status

Released on the VS Code Marketplace as nvitlam.agent-deck. Every part of all three engines — the readers, the tree builder, the live-status engines and the panel — is covered by an automated suite that runs against transcripts captured from real Claude Code sessions, a database captured from a real OpenCode one, and transcripts and hook payloads captured from real Codex ones. No live data directory of any of the three is ever read by a test.

About

Live observability for agent swarms in VS Code — Claude Code and OpenCode | The screen developers stare at while their agents work.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages