The Claude Agent SDK integration
How the desktop runs Claude through a Node agent host: the JSON-lines pipe, the HB Code tool bridge, the projection of Claude messages into the shared timeline rows, delivery proof, the run loop, subagents, and accounts.
Checked against the source on 5 October 2026
On this page
The agent host process
Claude runs through the agent host, a small TypeScript program in desktop/agent-host that uses @anthropic-ai/claude-agent-sdk. The desktop starts it with Node and passes the path of the packaged Claude executable as its only argument. A development build runs node from the PATH with agent-host/dist/src/entry.js. A packaged app runs its bundled node with claude/host/entry.mjs from its resources. The macOS build and the Windows builds package this runtime.
The two sides exchange one JSON object per line over stdin and stdout. A request carries an id of 1 to 200 characters, and its reply carries the same id with a result or an error. A line without an id is an event. The host checks every request against a strict schema, so an unknown field fails. The desktop gives each request 35 seconds. A request without a reply by then fails with “Claude host request timed out”.
| Request | Fields, limits, and what the host does |
|---|---|
| open | Fields: cwd, session |
| open | Fields: cwd, model, effort (optional), instructions, prompt, app |
| send | Fields: message |
| cancel | Fields: message |
| permission | Fields: request |
| tool | Fields: request |
| interrupt | No fields. Denies every open permission, stops every open HB Code tool call, and interrupts the turn. It also drops the messages that still wait for the turn, so none of them starts a new turn after the stop. |
| close | No fields. Closes the query. The host process exits when the desktop closes its stdin. |
| history | Also subagent |
| message | Fields: cwd, session |
| history | Fields: cwd, session |
| tool | Fields: cwd, session |
| list, inspect | Fields: cwd, and offset and limit 1 to 100, or session |
| subagents | Fields: cwd, session |
| catalog, usage, title | Fields: cwd for catalog. cwd and prompt, at most 40,000 characters, for title. Reads the model list, the plan usage, or a short session title. |
| Event | Fields and meaning |
|---|---|
| message | Fields: message. One SDK message as the query returns it: stream events, assistant and user messages, system messages, and the turn result. |
| permission | Fields: request |
| permission settled | The event permission |
| tool | Fields: request |
| tool | Fields: request |
| failed | Fields: error. The SDK stream threw an error. |
| closed | No fields. The SDK stream ended although nobody closed the session. |
- Run hostClaudeHost::spawn
Every run starts a new host with the session’s model, effort, and permission mode. It opens one SDK query in the project folder, resumes the saved conversation if there is one, and never outlives the run.
- Shutdownclose · stdin · killpg
At the idle state the desktop sends close and gives it 3 seconds, then closes stdin. If the host still runs 10 seconds later, the desktop kills it with every process that it started, so the claude child goes too.
- One-shot hostsClaudeHost::one_shot
History pages, command output, subagent transcripts, the final delivery proof, the model catalog, usage, and titles each start a host that answers one request and exits. During a run, the host of the run answers the delivery-proof reads. Transcript reads always use the main Claude folder.
- Inherited markersCLAUDECODE · CLAUDE_CODE_CHILD_SESSION · CLAUDE_PID
Removed with six more before every start. A desktop launched from inside Claude Code would otherwise run its sessions as child sessions, and Claude doesn’t save their transcripts.
Every run gets a new host, and the host never survives its run. When the run ends, the desktop sends close and waits at most 3 seconds for the reply. Then it closes stdin and gives the host 10 seconds to exit, and after that it kills the host and every process that the host started: the process group on macOS, and the process tree with taskkill on Windows. Reads that need no open conversation, such as the model catalog or a title, run in a one-shot host of their own. Before the start, the desktop removes nine environment markers that a parent Claude Code session gives its children, among them CLAUDECODE, CLAUDE_CODE_CHILD_SESSION, CLAUDE_CODE_SESSION_ID, and CLAUDE_PID. A desktop that you start from inside Claude Code would otherwise run its sessions as child sessions, and Claude doesn’t save their transcripts.
A request that fails the schema gets an error reply with its id. A line that the host can’t read as JSON gets a reply with the id null, and the desktop ignores it. The desktop buffers at most 256 events from one host. When the host’s stdout closes or a line is not JSON, the desktop fails every waiting request and makes a failed event itself. The host’s standard error goes to the desktop debug log.
- Settings and prompt: the query loads user, project, and local settings and uses the claude_code system prompt. The host appends the instructions of open to it. For each run, agents/session_rules.rs writes this text in this order: the questions rule, the session messages rule, the directions rule, and the text of the reply style. The questions rule says that Claude asks the user with AskUserQuestion and that the call doesn’t wait: the user answers in a later message. The session messages rule says that Claude reaches another HB Code session only with the hbcode tools send_session_message and maintain_sessions, because SendMessage and ListAgents reach only Claude Code sessions outside HB Code. The directions rule tells Claude to do all of an explicit direction, to ask one question, or to object with evidence. Simplified Technical English adds its reply text last, and Default adds no text. The app-tool text is not in this text: it is the instructions of the MCP server hbcode.
- Stream: includePartialMessages gives the live deltas, and forwardSubagentText sends each subagent turn in full. persistSession saves the transcript.
- Tools: the query gets the MCP server hbcode and its allowedTools. A PreToolUse hook answers every AskUserQuestion call at once. allowDangerouslySkipPermissions is set only for bypassPermissions.
- Run end: the host turns on Claude’s session state events with an environment variable, because a turn that starts background subagents ends before they do. Only the idle state tells the host that the whole run is over.
- Input: the query reads its messages from one stream that stays open for the whole run. The host writes each send into it at once, also while a turn runs.
The title request is a separate query. It uses the model haiku with no tools, one turn, no settings, and no saved session, and its system prompt forbids Claude to act on the message. The desktop asks for a title after send, in a one-shot host on the active account.
HB Code tools over the host
Codex and Claude get the same HB Code tools from one catalog in services/agent_tools. For Claude, the desktop builds the list for each run and sends it in open.appCapabilities: one instructions text and, for each tool, its name, description, input schema, and approval class. The host turns the list into an in-process SDK MCP server named hbcode, so Claude sees each tool as mcp__hbcode__<name>. A duplicate name or an input schema that is not an object fails the open.
| Approval class | Tools and who approves a call |
|---|---|
| read-only | Tools: maintain_ |
| desktop | Tools: send_ |
| agent | Tools: use_ |
The list depends on the settings. send_mobile_notification is there only while notifications are on, and synthesize_speech, create_explainer_video, and extract_video_context only with a Gemini key.
When Claude calls a tool, the SDK handler checks the arguments against the full schema, makes a new requestId, and emits toolCall. Then the handler waits. The event can’t choose its target. The desktop rejects a toolCall with any field besides requestId, toolUseId, tool, and arguments, and it routes the call by the session and the run that own this host. A rejected event or a second call with the same requestId fails the run.
- Tool listopen.appCapabilities
The desktop sends the instructions and each tool’s name, description, input schema, and approval class with open. The host builds the SDK MCP server hbcode from them.
- CalltoolCall { requestId, toolUseId, tool, arguments }
The host keeps the SDK tool handler open under a new requestId. The event can’t name a session or a run. The desktop routes the call by the run that owns the host.
- Tool taskAppToolCalls · toolResult
Each call runs in a task of its own beside the loop, and the task sends toolResult itself. If that request fails, the run fails.
- CanceltoolCancel { requestId }
On Stop or an aborted call, the host answers Claude with “Run stopped.” and sends toolCancel. The desktop aborts the task, and a result that crosses the cancel is ignored.
Each call runs in a task of its own beside the run loop. A slow tool therefore can’t block Stop, approvals, timeline rows, or the result of another tool. The task runs the shared tool handler with Claude’s tool use id as its call id and then sends toolResult itself. A tool that fails returns a result with success false, and Claude sees a tool error. If the toolResult request fails, the run fails.
When the SDK aborts a call or the run stops, the host answers the handler with “Run stopped.” and emits toolCancel, and the desktop aborts that task. A result that crosses the cancel is ignored. At the end of the run the desktop stops every task that is left. In the timeline the desktop makes the title of an HB Code tool row from its request, for example “Create session” or “Revise explainer video”.
History and the shared timeline rows
The desktop never reads Claude’s transcript files itself. The host reads them with getSessionMessages and returns pages of at most 100 complete messages. It joins adjacent assistant envelopes that have the same API message id and drops messages with nothing to show. A page boundary is a message uuid, and when that message is gone the reply asks the desktop to reload. A conversation without a transcript loads as empty. The SDK returns a compacted conversation from its last compaction on, so older turns of such a conversation don’t appear in HB Code. A command’s full output is read on demand through toolOutput, also for a subagent, the same way a Codex protocol output is read.
The cursor for older pages is a JSON object with session, before, anchor, and revision, at most 4096 characters long. The desktop checks it against the conversation or the subagent that it pages. A cursor for another target fails with a conflict that asks you to reload the conversation.
Reads are not cheap. Every transcript read starts a one-shot host, and getSessionMessages reads the whole transcript, because the SDK call has no range. A timeline load runs two hosts side by side: one for the page and one for the subagent list. A delivery proof is one messageSaved read. During a run the host of the run answers it. After the run a one-shot host answers it.
- Transcript pagegetSessionMessages · 100 messages
The host reads Claude’s saved conversation and returns at most 100 messages per page. A page starts at a message uuid, and when that message is gone the desktop reloads.
- One API messagemessage.id
Claude saves one line per content block. The host joins adjacent assistant lines with the same API message id, so a message keeps one identity whether it is live or saved.
- Tool resulttool_use_id
Arrives in a later user line, and the adapter completes the call row in place. A Bash result becomes an output row that is read from the transcript only when you open it.
- Hidden linesisSynthetic
Synthetic user messages, meta lines, and empty thinking make no row. Terminal housekeeping such as /exit is hidden too.
The Claude adapter projects each message into the same timeline rows that Codex uses. A row takes its identity from stable data only, so the saved row replaces the live one in place.
| Row | Identity key |
|---|---|
| A message that HB Code sent | user-message:: |
| Reply text | {message |
| Thinking | {message |
| Tool call | tool:{tool |
| Command output, or a result whose call is on an older page | tool:{tool |
| Image in a tool result | tool:{tool |
| Any other saved user row or note | The uuid of the saved message |
The SDK sends a final envelope for each content block. Such an envelope replaces its own text fragment, so a delta is never added twice. Saved rows sort by their position in the transcript, and live rows by arrival. A live view also sends the rows that it showed before and no longer shows, so every client removes them. A live prompt row exists only for a message that HB Code sent. Its row shows the display text and the attachments from claude_operations. The text that Claude gets is longer, because the desktop adds the skills context and the attachment paths. Every other prompt shows from the saved transcript.
| Claude message or block | Timeline row |
|---|---|
| Prompt | A user row. Pasted images become its attachments, and <system-reminder> blocks are removed. |
| Bash tool call and its result | A command row with its output. The output row names the saved result, which is read only when you open it. A failed command takes its exit code from the first result line “Exit code N”. |
| File edits | Edit, Multi |
| Read | A tool row titled with the file name. Its detail shows the file text without <system-reminder> blocks and opens the file. |
| Any other tool | One row that its result completes in place: Tool call, then Tool output, Tool error, or Tool declined. It shows a readable label, such as “Search in files” for Grep, and an MCP tool shows as “server: tool”. A result whose call is on an older page is a row of its own, Tool result or Tool error. A result block that is not text shows as its kind in brackets. |
| Tool call without a saved result | Finished when a later assistant message arrives. A command then shows “Claude did not save the output of this command.” |
| Send | A message row “Message to <name>” from the call on. When the call ends, the row shows Sent or Not sent. A message to a subagent of this session links to the subagent thread at the time of the call. A call without a recipient or without text stays a tool row. |
| HB Code tool | A tool row with a title that the desktop makes from the request, for example “Create session”. A finished send_ |
| Question for the user | Ask |
| Plan tools | Todo |
| Image in a tool result | An image row after the tool row. |
| Thinking | A Reasoning row. Empty thinking makes no row. |
| Interrupt note | A turn aborted marker. |
| Compaction summary | A “Conversation compacted” marker. |
| Local slash command | Its name and its output. Terminal housekeeping such as / |
| Message from the lead agent or from another Claude session | A message row: “Message from Claude” for the lead agent, and “Message from <name>” for another Claude session, else “Message from Another Claude session”. |
| User message inside a subagent’s view | A message row: “Task from Claude” for the first one, then “Message from Claude”. Each one links to the main timeline at its time. |
| Reply that Claude writes itself, such as a usage limit or an API error | A system row. |
| Rows without content for you | Hidden: a synthetic user message (is |
| Call that starts a subagent or a workflow | No row. The call and its result leave the view. The agents list shows the subagent or the workflow. |
Find and continue a saved conversation
To continue an existing Claude Code conversation, the desktop lists the saved conversations of exactly that workspace folder, newest first. The host calls listSessions with includeWorktrees false and includeProgrammatic false. A conversation from another checkout or worktree can’t resume in this folder, so it isn’t listed. A page holds 25 conversations by default and 100 at most, and its cursor is an offset into Claude’s own sorted list. Each row carries linkedAppSessionId when an HB Code session already owns the conversation. A title shows only when it says more than the first prompt.
A preview reads one page of the transcript. It binds nothing, writes nothing, and has no older pages.
When you continue, inspect must find the transcript in this exact folder. The new session takes Claude’s own name for the conversation, the custom title or else the summary. The table claude_sessions then binds the two ids. It has one row per session, and native_session_id is unique. A conversation therefore belongs to at most one HB Code session, and choosing it again opens that session instead of a second one. If that session belongs to another folder, the request fails with a conflict that names the session. A copy of a session gets a new conversation id. Transcript reads need no sign-in, and they always run with the default account’s folder.
Models and effort
The model list comes from a one-shot catalog host, one read per account and workspace folder. The desktop keeps a list for 10 minutes and clears all lists on a sign-in, a sign-out, an account switch, and an account removal. Claude lists aliases such as default and opus beside pinned model ids. The desktop keeps one row per resolved model id, and the alias default only marks the model that the account recommends.
A model’s default effort is high when the model offers it, and otherwise its first level. A model without effort levels gets the effort default, and open then carries no effort field. A new conversation starts with the recommended model. A continued conversation starts with the model that answered last, if the catalog still has it.
Live runs, delivery proof, and approvals
agents/dispatch sends a queued message to the adapter of the session’s agent. A Claude session can’t replace the latest turn or use a speed mode. It can steer a running turn. A session has one active Claude run at a time. The registry RUNS holds one run per app session, and a second run fails with “The Claude session already has an active run.”
The run opens the host with the session’s model, effort, and permission mode, records the operation, and then sends the text. The record is one row in claude_operations with a new UUID for the message. The insert keeps an existing row, so a retry of the same operation keeps the first UUID. A retry with another text or other attachments fails as a conflict. A retry from another run reads the transcript first. When the transcript has the message, the run sends nothing and the operation counts as sent. When it does not, the operation moves to the new run and goes out with the same UUID. The host of the earlier run has stopped, so Claude can’t take the message from it any more.
A message that you send while the run is active is a steer. The run loop owns every write to the host, so the steer goes to the loop. The loop records the operation with a UUID of its own and sends it. Claude reads the message after its current step, between two tool rounds. When the turn ends first, the message starts the next turn of the same run. The outbox entry stays sending with an acceptance that names the run, the Claude session ID as the thread, and the Claude ID of the message that started the run as the turn. A steer that finds no active run, or a run other than the one it names, is deferred, and the message stays queued.
Stop and send now takes a joined message back. The loop sends cancel to the host. When Claude has not started the message, the desktop stops the run and sends the same operation as a new run. When Claude has the message already, nothing is sent again.
Claude’s live stream never echoes the prompt, and a stdin write or a delta is never proof of delivery. Claude saves the message that starts a run before it calls the model. So the first message frame after send starts one transcript read. After a read that found nothing, each frame that names the message starts another. A frame names a message in user_message_uuid or user_message_uuids, or as a command_lifecycle frame with the state started or completed. The thinking counter and the idle frame start none. Claude saves a joined message later, with its next reply. So the frame that says that Claude started the message starts a read, and each of the next 3 replies of the conversation starts one more. When a read finds a message, the saved row appears first and the outbox row goes after it. At run end, one more read for each message without proof is the final proof, also for a stopped or failed run. A message that started the run and that no read finds is never sent again automatically. A joined message that no read finds returns to the queue.
- Operation uuidclaude_operations
Each message gets a new UUID, which the host sends as the SDK user message uuid. The desktop saves the pair, and a saved user line with that uuid is the only proof.
- First readfirst message frame
For the message that starts a run, the read starts on the first frame after send. Claude saves that message before it calls the model, so this read usually finds it before the reply starts.
- Stamped frameuser_message_uuid · user_message_uuids
A frame that names the operation’s uuid in user_message_uuid or user_message_uuids. It proves nothing by itself. After a read that found nothing, such a frame starts another read. The thinking counter repeats the uuid many times a second and starts none.
- Final readreconcile_operation
Runs after the host shuts down, for finished, stopped, and failed runs alike. A message that started the run and that no read finds is never sent again automatically. A joined message that no read finds returns to the queue.
| Access level | Claude permission mode |
|---|---|
| read-only | plan |
| full-auto | accept |
| full-access | auto. The open fails when the model doesn’t support Auto mode. |
| unrestricted | bypass |
When Claude asks to use a tool, the host holds the SDK’s canUseTool promise and sends a permission event. The desktop shows the same approval card as for Codex, with Accept and Decline and no other decision. A Bash call is a command approval, and every other tool is a permissions approval. A phone answers through run.chatResolveApproval, and the dispatch lets the Claude adapter answer first when it holds that request id.
- canUseToolapprovals.ts
The SDK asks before each tool use that the permission mode doesn’t allow by itself. The host keeps the promise open, and Claude waits.
- Approval cardpermission { requestId, toolUseId, agentId }
The desktop shows the same card as for Codex, and phones get it as a live event. A Bash call becomes a command approval, and a subagent’s request names the subagent.
- Answerrun.chatResolveApproval
A phone and the desktop take the same dispatch path. The Claude adapter checks the thread, turn, and item, then sends the host a permission request with allow or deny.
- Stopinterrupt
The host denies every open request with “Run stopped.”, and the cards end as Cancelled when the run ends. A question (AskUserQuestion) never opens a card: the host answers it at once, and the user answers in a later message.
- Reason text: for ExitPlanMode the card shows “Claude wants to carry out this plan:” and the plan. For Bash it shows Claude’s own description of the command. For another tool it shows the description, or else the tool name with its file path or URL, or else the tool name with its input as JSON.
- Subagents: a subagent’s request starts with “Requested by” and the name that the agents list shows.
- Answer: the adapter checks the thread, the turn, the item, and the decision. When no run holds the request any more, the answer fails with “The Claude run has ended.”
- Decline: Claude gets “The user declined this action.”, and the tool row becomes Tool declined.
- Stop: the host denies every open request and stops every open HB Code tool call with “Run stopped.” The end of the run marks the remaining cards as cancelled.
- Questions: a question never holds the run. A PreToolUse hook in the host answers AskUserQuestion at once in every permission mode, and Claude gets the notice that the question is shown and that the answer arrives as a later message. The timeline shows the call as a “Question for you” row, its options put an answer in the composer, and phones get a notification. No card opens.
- Duplicates: the host denies a second request with a request id that is still open.
The run loop, failures, and Stop
After send, one loop owns the run. It waits on seven inputs at the same time and handles the one that is ready first.
| Input | What the loop does |
|---|---|
| A tool task finished | Forgets the task. A task error ends the loop as a failure. |
| The stop signal | Closes the event channel, sends interrupt to the host, and leaves the loop as cancelled. The interrupt drops the messages that still wait for Claude, and the exit returns them to the queue. |
| The heartbeat, every 2 seconds | Calls touch_ |
| A proof read settled | When the read found the message, it adds the saved user row, publishes the rows, and then removes the outbox row. |
| A request for the running turn | A steer or a take-back from another task. The loop sends the steer to the host with send, or asks the host to cancel a waiting message, and answers the task. |
| The wait after a rest, 10 seconds | Runs only after Claude rested while a joined message still waited. When no frame arrives in that time, the loop ends. |
| A host event | tool |
Two message frames get special handling. The result frame records the success or the failure of the turn, and the loop goes on. The idle state ends the loop, with one exception. Claude can report that it rests just before it reads a message that arrived at that moment, and it then starts a turn for that message. So when a joined message still waits for Claude and the run has no failure, the loop goes on and waits 10 seconds for the next frame.
- Before sendProvenUnadmitted
Spawn, open, the Auto mode check, a second run, a record conflict, or a Stop before send. Claude did not get the message.
- From send onAdmissionUncertain · reconcile_operation
Claude can have the message. The final read decides: a saved message counts as sent, also after a failure. A message that no read finds is never sent again automatically.
- Select looptokio::select! · touch_chat_run
Seven inputs: a finished tool task, the stop signal, the 2 second heartbeat, a settled proof read, a request for the running turn, the 10 second wait after a rest, and a host event. The idle state ends the loop, unless a joined message still waits for Claude. The result frame does not.
- Exit orderturn.rs · execute
The same after idle, Stop, and a failure: tool tasks, host, approval cards, subagent views, final reads, terminal event, and the return of joined messages that Claude did not take.
The exit order is fixed, whatever ended the loop.
- The desktop closes the event channel and stops every tool task that is left.
- It shuts the host down: close, 3 seconds, stdin closed, 10 seconds, kill.
- It marks every open approval card as cancelled.
- It settles every subagent view that is still open: as aborted after a stop or a failure, and as completed otherwise.
- It runs the final proof reads: first for each joined message without proof, then for the message that started the run.
- It sends the terminal event of the run: aborted, failed with the reason, or completed.
- It returns each joined message that the transcript does not have to the queue.
Then the desktop forgets the account’s usage reading, finishes the run in the run registry, and removes it from RUNS. The send boundary decides what a failure means. ProvenUnadmitted says that Claude did not get the message. AdmissionUncertain says that Claude can have it, so the saved history must decide before a retry.
| When | Failure and outcome |
|---|---|
| A steer | No run is active, or the active run is not the run that the steer names. Outcome: Deferred. The message stays queued. |
| A steer | The send request to the host fails, or the run’s task stops while it gives the message to Claude. Outcome: Admission |
| Before send | No account folder, a runtime that is not packaged, a spawn error, an open error such as a model without Auto mode, a second run for the session, a child thread as the target, or a record conflict. Outcome: Proven |
| Before send | Stop arrives after open. Outcome: Proven |
| Before send | The operation has an earlier attempt from another run, and the transcript read for it fails. Outcome: Admission |
| At send | The send request fails or gets no reply in 35 seconds. Outcome: Admission |
| In the loop | A failed event, “Claude closed before the run completed.”, “The Claude host disconnected.”, a tool task error, or a result with is_ |
| At the end | The final read does not find the message that started the run. Outcome: Admission |
| At the end | The final read does not find a joined message. Outcome: The ledger marks the operation as canceled, and the entry returns to the queue with automatic drain off and the text “The active run stopped before this message reached the agent. Review and send it again.” |
| At the end | The host shutdown or the final read fails. Outcome: Admission |
The reason of a failed result comes from Claude’s errors list, or from its result text for API errors such as a missing sign-in. A failed run keeps its reason in the desktop error log under claude_run, with the session, run, and operation ids. Packaged apps discard standard error, so the log line alone would be lost.
A cancel answers only for the exact run id. It sets the stop flag and waits until the run reports that it is done, which happens after the host has exited. If the run’s task died and can no longer report, the cancel itself shuts that host down, finishes the run, and removes it from RUNS.
The shared run registry reclaims a run as stale when it shows no activity for 45 seconds. A Claude host can need longer than that to open a conversation, and the first heartbeat comes only from the loop. So the registry asks the Claude adapter first, and a run whose exact host process is alive is never reclaimed.
Subagents as child threads
Claude subagents use the same list, timeline, and live paths as Codex child threads. During a run, Claude reports each subagent through task events and forwards its turns with the id of the spawning tool call. Only a task_started event with the task type local_agent makes a subagent. The adapter sends the forwarded turns to the subagent’s own view. The call that started the subagent and its result leave the parent’s view, so no row stands for the subagent. A subagent that the conversation started has depth 1, and each nesting adds 1. A live subagent carries no parent id, so the depth of a nested subagent is known only from its saved transcript.
| Claude task status | Thread status |
|---|---|
| pending | Pending |
| running, paused | Running |
| completed | Completed |
| failed | Errored |
| killed, stopped | Interrupted |
A workflow is a script that the conversation starts with one call of the Workflow tool, and the script starts subagents in phases. The running turn follows a workflow as one task: a task_started event with the task type local_workflow, which gives the id of the starting tool call, the workflow name, and a summary. The later task events of that task change its status as the table above shows. Claude does not forward the turns of a workflow’s subagents, and no task event names them. They appear in the list only through their saved files.
- Turn resultresult
Records success or failure and nothing else. Background subagents keep running, and Claude answers each finished report with a follow-up turn.
- IdleCLAUDE_CODE_EMIT_SESSION_STATE_EVENTS
Set by the host on the session query. Only the idle state ends the run. Then the desktop shuts the host down and runs the final delivery read.
- Subagent viewparent_tool_use_id
During the run, each subagent’s turns go to its own view, addressed like a Codex child thread. The parent gets one spawn row that links to it. The agents list uses the row, and the chat timeline does not show it.
- After the rungetSubagentMessages
The saved transcripts and the files beside them are the only source. A view without a finished task event is closed: aborted after a stop or failure, completed otherwise.
The saved files are the source for every subagent that the running turn does not report, and after the run they are the only source. The SDK gives the list of subagent ids and each transcript. The host also reads three kinds of Claude files itself, because the SDK exposes none of them.
| File | Path in the session folder and content |
|---|---|
| Sidecar | agent-<id>.meta. |
| Run record | workflows/ |
| Transcript | agent-<id>.jsonl itself: the effort field of the first saved assistant line, and the file size in bytes. |
Claude names a project folder after the working directory by a rule of its own. So the host looks in every folder under projects in CLAUDE_CONFIG_DIR, or under ~/.claude/projects, for a folder with the session id that has a subagents folder with entries. Each reader returns nothing when a file is absent or has another shape, and the list then shows fewer facts.
One subagents request reads all of this in a one-shot host. A transcript read costs the full file, so the desktop names what it already holds, and the host reads only the rest. The desktop keeps these facts per conversation until it quits.
| Field | Content |
|---|---|
| Request: known | Each ended subagent that the desktop holds, as agent |
| Request: ended | The ids of the workflow runs with a record. They don’t change again, so the host skips them. |
| Request: open | The ids of the runs without a record that the desktop holds. The host reads the main conversation, the largest file, only for a run that is in neither list, or for a subagent without a sidecar description. There it finds the Workflow call of the run and the input of each spawn call. |
| Result: ids | The id of every subagent of the conversation. |
| Result: subagents | The facts of each subagent that the host read. |
| Result: run | The id of every workflow run, from the folder names under subagents/ |
| Result: workflows | For each run that is not in ended |
| Subagent field | Source |
|---|---|
| Identity | agent |
| Name and kind | description and subagent |
| task | The first user message without its workflow frame, or else the start of the task in the run record. |
| model | The model of the last assistant message that is not an API error. |
| effort | The effort field of the first saved assistant line. |
| tokens | The size of the context at the last assistant turn: input_ |
| tool | The number of tool_ |
| run | The workflow run from the folder name, and workflow |
| started | The timestamps of the first and the last transcript message. |
| size | The size of the transcript file in bytes. |
| outcome | completed, failed, stopped, or null while the subagent can still change. |
The host decides the outcome in this order. The run record says completed for the state done and failed for the state error. Otherwise a last message with the model <synthetic> is an API error and means failed. Otherwise a last assistant message with stop_reason end_turn, with text, and with no tool call means completed. Text that comes before a tool call in the same turn does not finish the subagent. Otherwise a subagent of a run that has a record is stopped, because the run ended before the subagent did. Every other subagent has the outcome null.
A workflow run takes its name, summary, phases, status, start, and duration from its record. A run without a record is still open. The line “Run ID:” in the result of its Workflow call then links the run to that call, and the line “Summary:” gives its summary. Its name and its phase titles come from the meta statement of its script.
A workflow gives each subagent its task inside a frame. A frame is one line that starts with “[Workflow harness — user request]” or “[Workflow harness — computed task]”, and the text follows with each line indented by two spaces. The host removes the frame line and the indent for the task field. subagentHistory returns the computed task without its frame and leaves out the framed user request, because the conversation already shows that request.
The desktop then gives each subagent a status. The first rule that applies wins.
- The running turn of this session reports the subagent as a task: the status of that task.
- The files give an outcome: completed is Completed, failed is Errored, and stopped is Interrupted.
- The subagent belongs to a workflow that the running turn reports as running: Running.
- The last transcript message is less than 10 minutes old, and it is later than the end of this session’s last run in this desktop run: Running. Another Claude app can run the same conversation, so recent activity is the only sign.
- Otherwise: Interrupted.
The 10 minutes come from Claude’s longest normal silence, a shell command, which Claude stops after 10 minutes at most. A workflow run takes the outcome of its record, or else the status of its task in the running turn, or else rule 4 with the latest activity of its subagents. A subagent that the turn reports and that has no transcript yet is added to the list from its task event. Subagents that are still open when a run ends stop with it, so activity from before that end does not count.
The list result uses the agent thread contract that Codex child threads use. Claude fills fields that Codex leaves empty.
| Field | Content of the AgentThread field for a Claude subagent |
|---|---|
| Workflow | workflow |
| usage | tokens, which can be absent, and tool |
| reasoning | The effort. |
| agent | The subagent type. Claude’s default kinds general-purpose and workflow-subagent are not sent. |
| title, task | The title is the description, or else the first line of the task that is not empty, or else “Subagent”. The task in the list is cut after 240 characters, and the subagent’s own view has the full task. |
| Parent and depth | parent |
| can | Always false. Only the parent agent writes to a subagent, and a send that targets a child thread fails before the host opens the conversation. |
| Workflow list | workflows in the list result, one entry per run: workflow |
No timeline row stands for a subagent or a workflow. The call that starts one and its result leave the view, and the agents list shows the subagents and the workflow runs. A task event that changes the list, and the end of a run with open subagents, send the agent-threads signal. Each client then loads the list again. On the desktop, the agents list groups workflow subagents under their workflow and phase and shows model, effort, tokens, tool calls, and duration.
Accounts and usage
The default account is this computer’s Claude sign-in and runs with Claude’s own folder. That folder is ~/.claude, or the folder in the desktop’s own CLAUDE_CONFIG_DIR. Each added account is a folder claude/accounts/<uuid> in HB Code’s local application data. It keeps its own sign-in and links every other entry to the main folder. All accounts therefore read and continue the same sessions with the same settings, and a switch changes only who pays for a turn.
- Default account~/.claude · CLAUDE_CONFIG_DIR
This computer’s own Claude sign-in. Its folder is ~/.claude, or the folder in the desktop’s own CLAUDE_CONFIG_DIR. HB Code sets no folder for it.
- Added accountclaude/accounts/<uuid>
A folder in HB Code’s application data. HB Code runs claude auth login --claudeai with CLAUDE_CONFIG_DIR set to it, and Claude keeps the credential. On macOS the Keychain item belongs to the folder.
- Linksfolders.rs
Every top-level entry except .claude.json, its lock, .credentials.json, backups, and statsig links to the main folder. An entry that the account folder already has is never overwritten.
- Usagefive_hour · seven_day
Read per account through the SDK: the five-hour window, the weekly window, and weekly windows per model. One reading per account per minute serves the sidebar, settings, and phones.
The setting agent.chat.claude_account_id names the active account, and no value means the default account. The next turn runs on the active account, and a removed account leaves the default account active. The desktop makes the links before every start of a Claude process. It creates projects in the main folder first, so an added account never starts a transcript store of its own. A projects link that fails stops the start. Any other link that fails is only logged. On macOS each link is a symbolic link. On Windows a folder gets a directory junction, and a file gets a symbolic link, or a hard link when Windows refuses the symbolic link, so no Developer Mode is needed.
HB Code runs Claude’s own sign-in commands for one account folder. It never reads a token or runs its own authorization flow.
| Command | Deadline and notes |
|---|---|
| claude auth status --json | Deadline: 15 seconds. Claude exits with an error code when it is signed out and still prints the same JSON. |
| claude auth login --claudeai | Deadline: 600 seconds. One sign-in runs at a time, and you can cancel it. A second sign-in to an account that is already on this computer is signed out and removed again. |
| claude auth logout | Deadline: 15 seconds. Removal signs the account out and then deletes its folder. It is refused while a run uses the account. |
Usage comes from the SDK: the rolling five-hour window and the weekly windows. The weekly window for third-party apps is left out. An API-key account has no plan windows. The usage host loads no settings and skips the scan of local transcripts. Usage is read only for a signed-in account, and a read that fails shows as an error text on that account while the list still loads. For the weekly windows per model, the server’s own labels come first, and the fixed Opus and Sonnet fields fill in a model that it did not list. Reset times are rounded to the nearest minute, and each reading goes to the usage history. The sidebar, the settings page, and the phones share one reading per account per minute, and a finished run clears that account’s reading.