Skip to content

@ahpd/agent-claude ​

npmCIlicense MITnode >=22Agent Host Protocol 0.9.0

Claude Code as a backend for @ahpd/sdk, and the plugin that lets the @ahpd/server daemon run it.

The plugin registers the engine claude: every Claude Code agent is a record on the agents channel, x-ahpd-agents, and one load serves all of them. A record's id is the provider a client names, and its config holds what that one agent runs on.

Part of ahpd. The source is in packages/agent-claude.

Install ​

bash
ahpd plugin install @ahpd/agent-claude
ahpd restart

Update, remove and one-run flags such as --plugin are in Plugins.

What it needs ​

  • Claude Code signed in on this host, with claude login, or ANTHROPIC_API_KEY in the daemon's environment.

Options ​

json
{
  "plugins": [
    { "name": "@ahpd/agent-claude", "options": { "paths": ["/work/project"] } }
  ]
}

The ordinary install takes no options: it lists every session the CLI's store holds, wherever each ran, and works in the directories the daemon was started on. Everything else has a default that fits most installs. The daemon loads the change on its next restart.

Every option below is a key under options in the plugin's entry, and the daemon checks each value against the schema before apply runs. These are the plugin's answers for every agent it serves, not any one agent's: what one agent runs on is its record's config, below. provider, displayName and name are not options at all - a record's id is the provider and its name is what a client draws.

OptionDefaultWhat it does
pathsthe host's directoriesThe directories it works in; the first is where a session goes when a client names none
computerExecutableclaudeWhere the CLI is inside a machine
computerClipartWhere the CLI a machine runs comes from
computerCliFallbackrefuseWith computerCli: "part", what a machine gets when the claude part cannot be built
computerConfigDir/ahpd/<id>, or /ahpd/claude for the first recordThe configuration directory the CLI reads inside a machine; false leaves the image's own
handshakeTimeoutMs120000How long the CLI has to answer its handshake, the bound the SDK's startup is handed
hostToolstrueWhether the host's own tools reach each session. A record can turn them off for one agent

computerCli is part, the claude part this host builds at its pinned version, or host, which mounts this host's own installed CLI. computerCliFallback is refuse, which refuses a Claude session in a machine and names the part, or host, which mounts this host's own CLI instead and logs that it did. handshakeTimeoutMs is how long a CLI has to take the control request that says it is up: a cold start with several MCP servers is slow, and a machine slower than that is what the option is for. A session starts its CLI through the SDK's startup, as VS Code's Claude does, and hands it this bound: a CLI that misses it did not start, the SDK closes it, the turn fails in the SDK's words, and the next send starts a CLI again.

A record of the engine ​

An agent is a record on x-ahpd-agents: { id, engine, owner, name, description?, icon?, model?, config }. The channel owns name, description, icon and model on every engine, so this plugin declares only the fields below, and a new record starts from the engine's one template - Claude Code with nothing set - so an empty config is the ordinary agent.

FieldDefaultWhat it does
modelsthe CLI's own listThe models that agent offers in the picker, each an { "id", "name" }
modelSourcesnoneEndpoints whose model lists that agent also offers, each { "fetch", "match", "key" }
keepCliModelsfalseWith either list, add them to the CLI's list rather than replace it
sandboxdefaultThe CLI's own sandbox for shell commands: default leaves it to the settings files, on and off set it
thinkingadaptiveExtended thinking: adaptive lets the agent decide when to think, disabled is none
outputStylenoneThe name of a style from the CLI's own settings
envnoneVariables for the CLI's process, laid over the daemon's own environment on this host, and the whole of it in a machine
loginsnoneThe Claude logins the agent runs on, by the id of a claude-login connector, in the order a turn tries them. See Several logins
extraArgsnoneArguments the CLI is started with beyond the ones this backend builds. Host records only
hostTaskstrueWhether Claude leaves its own task tools to the host's task_add. Off keeps them, and their calls write the session's plan
hostToolstrueWhether the host's own tools reach this agent's sessions. Off drops them and keeps each client's tools
enableTaskstrueWhether Claude's own Task tools are on when Claude keeps its task tools; off starts the CLI with CLAUDE_CODE_ENABLE_TASKS=0, which gives it TodoWrite instead
dontAsktrueWhether the Approvals control offers the SDK's dontAsk beside VS Code's five modes; off offers VS Code's five alone
taskOutputFirstWaitMs500The first wait between the reads that follow a background shell's output, in milliseconds, as VS Code's output monitor waits
taskOutputLongestWaitMs10000The longest wait between those reads, in milliseconds, each wait twice the last until it
taskOutputQuietReads2How many reads in a row that find nothing new count the shell quiet, so its next output brings the wait back to the first, as VS Code's output monitor counts
subagentTitleMax60The longest title, in characters, a worker chat takes from its task before it is cut, as VS Code's
subagentStartTimeoutMs30000How long, in milliseconds, a worker's frames are held for its spawn to be recorded, as VS Code's
maxShellInitScript65536The longest shell init script, in characters, a client may send, as VS Code's
maxTerminalOutputBytes10485760The largest saved shell output, in bytes, a call names a terminal for, as VS Code's
keepFramed64How many ids of the client tool calls the CLI framed are kept for a handler that runs late
watchWaitMs300How long, in milliseconds, the customization folders' watch waits after a change before it reads them again, as VS Code's Claude watcher
fileMostBytes65536The most of a customization file, in bytes, read for its frontmatter
ruleDepth32How many folders deep a rules folder is walked
ruleMost1000The most rule files read from one rules folder
cliStartopenWhen a session starts its CLI: open as the session is made, so its account, context and plan limits show before any turn; send at its first message, as VS Code's Claude agent makes a session provisional until then
stopEndsturnWhat a stop ends: turn interrupts the turn through the SDK and keeps the CLI, ending only the workers the stopped turn spawned, and a steering message the CLI holds is answered after the stop in a turn of its own; query aborts the query as VS Code's agent does, its workers, its tasks and the steering it holds with it
portFailuresspawnHow a machine's port that could not start the CLI is said: spawn files it as a failure to start, so the turn fails in the SDK's one sentence for that, with no host path; message passes on the failure's own message, as VS Code's host shows a failed send's
subagentsFrommetaWhere a restored session's worker chats are read from: meta reads the CLI's worker files and links each by the call its meta file names, which tells two identical delegations apart; sdk reads them through the SDK's listSubagents and getSubagentMessages and links each as VS Code's agent does, by the agentId line of its call's result, else its prompt, else its last answer

extraArgs is host-only: a team's or a person's record that sets one is refused, because an argument is where a command would come from. ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN in env are the engine's key variables, so any owner may set them; every other env variable is the host's alone, because a free environment can run a program.

A models entry is { "id": "<model>", "name": "<label>" }, and name may be left out, in which case the picker shows the id. A modelSources entry is { "fetch": "<url>", "match": "<pattern>", "key": "..." }, which reads an OpenAI-shaped model list and keeps the ids the pattern covers, where * is any run of characters. The written models come first and each source's after them, each id once. key is the bearer token sent with the fetch. A key written as text moves to the vault when the record is saved, and a client sees <set> in its place. Send it empty to keep it; an entry that changed needs its key again. A host record or a preset may set key to { "fromEnv": "NAME" } instead. A fetch that fails is logged, with no key in the line, and offers nothing. A record written before the two lists held one shape is split into them at the next start.

A record's env value is a string, null to unset the variable, { "fromEnv": "NAME" } for the daemon's own NAME, or { "$secret": "host:<name>" } for a credential kept in the vault. A fromEnv variable that is not there when the record opens is left unset.

ahpd vault set host:<name> keeps the credential a record's env reads with { "$secret": "host:<name>" }, from standard input. See PLUGINS.md for what a credential is.

An extraArgs value is given by name without the --, and null for a flag that takes none. A value that is not a string reaches the CLI as its JSON text, so "settings": { "permissions": { "allow": ["Read"] } } is started as --settings '{"permissions":{"allow":["Read"]}}'.

A record is checked against the engine's schema when it is put, and opened again on every start. One whose config no longer fits, or whose $secret the vault cannot answer, is written and shown as invalid with one line saying why and naming no value; a record whose engine no plugin registers any more is kept and shown as unavailable.

Tasks ​

A Claude session uses the host's task tools by default. When the tasks plugin is loaded and offers task_add, the session's own TodoWrite, TaskCreate, TaskUpdate, TaskList and TaskGet are denied. The model adds its tasks with the host's tool, so every session works one list. A host that offers no task_add has nothing to put in their place, and the session keeps its own tools there.

hostTasks: false in a record keeps Claude's own tools; so does a host with no tasks plugin loaded. The CLI's own Task tools are its default; enableTasks: false starts it with CLAUDE_CODE_ENABLE_TASKS=0, which gives it the legacy TodoWrite instead (environment variables). A value the record's own env gives the variable is kept rather than written over. Each TodoWrite, TaskCreate and TaskUpdate is then written into the session's plan, and The task list says how.

In your own host ​

bash
pnpm add @ahpd/agent-claude @ahpd/sdk @microsoft/agent-host-protocol
ts
import { createHost, listen } from '@ahpd/sdk';
import { claude } from '@ahpd/agent-claude';

const path = process.cwd();
const host = createHost({ path, agents: [claude({ paths: [path] })] });
await listen({ port: 9187 }, (peer) => host.accept(peer));

createHost takes a list of agents, so this can run alongside other backends. The plugin entry wraps the same claude().

Records ​

One agent is one record. A client makes or edits one with x-ahpd/agents/put, and the daemon keeps it at agents/<id>.json in its configuration folder. The host's Claude Code, with nothing set, is the record below; a second agent is another record from the same engine, and a session picks the agent rather than a set of options of it:

json
{ "id": "claude", "engine": "claude", "owner": "host", "name": "Claude Code", "config": {} }

A record that names its own options and its own endpoint is a second agent on the picker, from the one plugin. The key is a $secret, so the record holds a reference and the vault holds the value:

json
{
  "id": "claude-deepseek-build",
  "engine": "claude",
  "owner": "host",
  "name": "DeepSeek build",
  "config": {
    "thinking": "adaptive",
    "models": [{ "id": "deepseek-chat", "name": "DeepSeek Chat" }],
    "env": {
      "ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic",
      "ANTHROPIC_AUTH_TOKEN": { "$secret": "host:deepseek" }
    }
  }
}

A client puts the key's value, and the channel moves it to the vault under host:agent/<id>/env.ANTHROPIC_AUTH_TOKEN and keeps a { "$secret": "<name>" } in its place, so no client is shown the value again. The record below names host:deepseek, the form a record written by hand on disk uses, which may name any entry of the vault; ahpd vault set host:deepseek keeps that entry from standard input. The endpoint a record names is the one it is probed at, and an edit of the record reaches a running session at its next turn.

Project files, and trust ​

A session in a folder the host was not told to trust loads none of that folder's own files. Nothing of the project is loaded: no settings, so no hooks, no project CLAUDE.md, and no .mcp.json beside the project. The person's own files are not a project's and are read either way. Trust is the window's answer, pushed in the root config - see Trusted folders. A session nobody vouched for is untrusted, which is the same answer a host with no window to ask gives.

A second Claude on another endpoint ​

A record with its own name, its own models and an env that points the CLI elsewhere is a second agent on the picker, from the one plugin. A host record may name a daemon variable instead of a vault entry, which keeps the key where it already is, and fetch the endpoint's model list rather than writing it out:

json
{
  "id": "claude-openrouter",
  "engine": "claude",
  "owner": "host",
  "name": "Claude OpenRouter",
  "config": {
    "modelSources": [
      { "fetch": "https://openrouter.ai/api/v1/models", "match": "anthropic/*" }
    ],
    "env": {
      "ANTHROPIC_BASE_URL": "https://openrouter.ai/api",
      "ANTHROPIC_AUTH_TOKEN": { "fromEnv": "OPENROUTER_API_KEY" },
      "ANTHROPIC_API_KEY": ""
    }
  }
}

Both agents carry their key in the daemon's environment or in the vault, and the endpoint a record names is the one it is probed at.

What it does ​

It starts the Claude agent SDK, converts its message stream into AHP state actions, and reads Claude's transcript files.

export
claude(options)the Agent to pass to createHost
createSession(options)one live session
catalogue()every session Claude's store holds, wherever it ran, as rows a host can list
turnsOf(sessionId)a past session read from its transcript, as turns
probe(options)runs a CLI at startup to read the available models and commands
apply(host, options)the plugin entry, with name and title beside it

Supported ​

Turns and streaming, tool calls and approvals, questions from the agent, model and effort selection, permission modes, MCP servers and OAuth sign-in, skills and slash commands, multiple chats per session, forking a chat from a turn, truncating a chat back to a turn, resuming a failed turn, session titles, token usage, and context compaction.

An approval is the host's. Claude says what the call asks for beside its question, as VS Code's Claude agent does, and offers no options of its own, so the host's are put; the CLI is given the host's answer.

The operator's managed settings, Start.managedSettings, reach the CLI as Claude Code's own policy tier, the SDK's managedSettings, which the CLI holds on every call, the calls it answers without asking the host among them. A rule is written in Claude Code's grammar where a Claude Code rule names the calls it names, as VS Code's bridge writes a setting in its runtime's grammar: a Shell rule as a Bash and a PowerShell rule, a Read rule as Read and a Write rule as Edit, a path anchored where the host anchors it, and a Domain rule as WebFetch(domain:<host>). A rule Claude Code would read as naming other calls is left out: a read deny, which Claude Code applies to the path's edits and writes as well, a shell pattern the host reads as a regular expression or against the whole line, a path with brackets or braces, and limitTo. Each tool eligibleForAutoApproval sets false is an ask rule naming it by the CLI's name for it: <name> for the CLI's own tool, and mcp__<server>__<tool> for an MCP server's <server>/<tool>, the host's tools among them as host/<name>, on the server VS Code names host. A client's tool of a key's name the CLI calls mcp__client__<clientId>__<name>, one name for each client offering it, and a session's clients come and go under a policy tier the CLI is given once, as it starts, so no rule names that call, and a tool's name set false turns on the bypass lock below, as VS Code's bridge falls back to the lock for a tool no rule family names. Claude Code asks about a call an ask rule names in every mode, bypassPermissions included, and the managed disableBypassPermissionsMode is Claude Code's own, which takes bypassPermissions away. Under that lock a session's Approvals control offers Ask Before Edits and Plan Mode, with Don't Ask where the record offers it, the modes VS Code's window leaves open for Claude under the auto-approval restriction it sets from the lock, and a session holding another mode runs Ask Before Edits. On a machine whose administrator deploys Claude Code's managed settings, the CLI takes these where that policy sets parentSettingsBehavior to merge.

A subagent Claude runs is its own chat. Every Task and Agent call opens one through the host's Start.subagent, read-only and named ahp-chat://subagent/…, and the subagent's text, thinking, tool calls and permission asks are drawn there instead of in the turn that spawned it. A stop given in the subagent's chat stops that subagent through the SDK's stopTask, and the turn that started it goes on and sees the call end. Before Claude has named the subagent's task, the stop stops nothing else. On a host without Start.subagent the subagent's output stays inline. A session read back from disk rebuilds each subagent's chat from the CLI's subagents/*.meta.json and .jsonl files.

A Bash result too large to send shows its whole output in a terminal, as VS Code does. Claude gives the model a notice in place of such an output and saves the output to a file, which the result's tool_use_result.persistedOutputPath names. The call keeps the notice as its text, the host keeps the file's text for the call's chat through Start.terminalOutputs, and the call's content names the terminal the host rebuilds from it: exited, isPty: false, with exitCode: 0 for a command that worked, Claude's preview and truncated: true. It lasts as long as the chat, and goes with its turn, its chat and its session. Claude's transcript holds the notice alone, so a call read back names its terminal again where the host still keeps the output. A file over 10 MB is not read. A subagent's result, a backgrounded command, a session in a machine and a host whose session store keeps no folder get no terminal.

Sessions the host is not running are read from Claude's transcripts, so clients can browse and read them without starting a process. The agent starts when a turn is sent.

A session starts no CLI until its first send, as VS Code's Claude agent keeps a session provisional until its first send materializes it. Until a CLI answers what it has, the session lists the agent's own models and customizations, which the host seeds it with, and what its disk scan finds: each file of its scopes, under the name the CLI knows it by, and each MCP server a settings file declares, stopped. A switch of a server or a plugin made before then is the first CLI's: it starts with the plugins switched in its settings, and its servers are switched before the first prompt goes out, as VS Code's send brings a CLI to a session's switches. A start or a stop of a server is refused, there being none running. A CLI that does not answer what it has leaves the lists as they were, and the next CLI the session starts is asked.

A question somebody answered reads back answered, in the session and in a subagent's chat. The CLI writes the answers as the toolUseResult of the transcript entry that holds the question's result. The SDK's getSessionMessages leaves that field out, so a session's own file is read for it, one line at a time.

A turn that failed on a model call can be resumed. The turn reopens with its message and its parts, the conversation is cut back to the end of the turn before with resumeSessionAt, and the prompt is sent again. A conversation whose first turn failed has nothing to cut back to, so it is deleted and started over under its id. The calls the failed turn ran run again, because the cut drops them with the prompt. Only the latest turn, in error, whose last part is a resumable error, can be resumed, and only while the conversation has a turn to cut back to. A turn refused before the CLI, a turn whose CLI went under it and a login the CLI refused end with no resumable error, and a resume whose conversation cannot be started over ends the turn with the reason and no resumable error either.

A session the host resumes picks up the conversation the SDK holds under that id, in any project folder, asked of the SDK's getSessionInfo as its first CLI starts, as VS Code's agent host asks as it materializes a session at its first send. The CLI writes a conversation when its first message reaches it, so a chat nobody spoke to, or one whose CLI could not sign in, has none, and the CLI refuses to resume it. Such a session starts a new conversation under that id, under the chat's own id where it has one, as VS Code's agent host does. The CLI starts a conversation under an id only when its folder's project holds no transcript for it, so a transcript there in which the SDK finds no session is deleted first, through the SDK's deleteSession and once the CLI that could write it has exited, as VS Code's agent host shuts the live CLI down and deletes a transcript through the SDK before it starts a conversation over under the same id.

Where a customization comes from ​

The CLI reports the names of the skills, commands and agents it loaded, and not their files. The backend reads the folders the CLI reads, finds each name there, and lists the entry in the directory of its scope with its file as the uri, in the tree VS Code's Claude discovery lays out. A command is a skill: its file in commands is listed among the skills of its scope. Before a session's first send the names are those of the files themselves, and an entry with no description from the CLI takes its file's own.

ScopeDirectoriesA person writes there
project<cwd>/.claude/agents, skills, rules and hooksyes
userCLAUDE_CONFIG_DIR, or ~/.claude, with the same fouryes
managed.claude/skills and .claude/agents in /etc/claude-code, /Library/Application Support/ClaudeCode or C:\Program Files\ClaudeCodeno
synced<config dir>/skills/synced/<id>/<name>/SKILL.md, listed by each manifest.jsonno
builtinagent-builtin:/skillsno

A skill found in more than one scope takes the managed file first, then the personal one, then the project one; an agent takes the managed file, then the project one, then the personal one. A synced skill is matched by anthropic-skills:<name> and by its bare name, and listed once. A skill or command with no file is one the CLI ships, and goes in the builtin skills directory with the uri agent-builtin:/skill/<name>; an agent with no file is the CLI's own, and is listed among the personal agents with the uri claude-internal:/agent/<name>, <name> being the name the CLI runs it by, a plugin's namespace among it. A plugin the CLI loaded from a root on this disk is a container id'd by its root's URI and named by its enabledPlugins id, the source the CLI reports, holding what its directory holds as VS Code lists a plugin: its agents, skills, rules, hooks file and MCP servers, read off its .claude-plugin/plugin.json and the places it names, each by its file's URI. What the CLI reports of them is listed there alone, and one the plugin holds no file for as an entry with no file; a plugin's commands are among the built-in skills. A plugin the CLI ships has no root and no container, as VS Code lists a plugin it resolves to a root, and what the CLI reports of one is listed as the CLI's own. A plugin's switch is the CLI's enabledPlugins setting under that id. Every entry's id is its uri, as VS Code's agents mint one, and a skill's argument hint is _meta['vscode.skill.argumentHint'].

A scope's hooks directory lists each of its settings files that declares a hook: the working directory's .claude/settings.json and settings.local.json for the project, the personal settings.json for the user. A file declares one when it names a command for a lifecycle event and does not say disableAllHooks.

An MCP server the CLI reports is listed at the top level, named by the file that declares it, as VS Code's Claude names one: the working directory's .claude/settings.json, then its .mcp.json, then the personal settings.json, the first file to declare a name taking it. The server's uri is that file, and its id the file's uri with #mcp=<name>. A server no file there declares has the uri claude-internal:/mcp/<name> and the id claude-internal:/mcp/<name>#mcp=<name>.

A session started in the home folder has no project scope, because its .claude is the personal folder; the home folder's CLAUDE.md is then a personal rule.

The project and user directories of agents, skills and rules are sent with nothing in them too, so a client can offer to write the first one there. A hooks directory and the read-only directories are sent only with something in them.

The rules are the CLAUDE.md files and every *.md under a rules folder: <config dir>/CLAUDE.md, and CLAUDE.md, .claude/CLAUDE.md and CLAUDE.local.md in the working directory. A rule whose frontmatter has paths carries them as globs; any other rule is alwaysApply.

A file's frontmatter is what lies between the --- line that opens it and the next --- line, as VS Code splits one, read as YAML with the host's own reader, the parseYaml the plugin context hands every plugin. Only a file's first 64 KB is read.

A session in a machine has its CLI read the machine's own config volume. Its entries are listed with no file: its skills and commands in the builtin skills directory, its agents in a read-only builtin agents directory, claude-internal:/agents. No writable directory is sent.

A file written, edited or deleted in these folders shows without a restart. Each session watches its personal and project folders from the moment it is made, reads them again before its first CLI has answered and asks the CLI for its lists again after, and the host does the same for the personal folder before any session exists. A session in a machine is not watched. Sessions and presets that watch the same folder share one watch of it.

The agent a message picks ​

A client sends the agent on every message, as message.agent, and the turn runs on it. A message naming none runs on the SDK's default agent.

A uri becomes the name the SDK is given: an agent's own file, file://…/agents/reviewer.md, gives its frontmatter name, or the file's own name when it has none or has been deleted since the listing was made; a built-in agent's claude-internal:/agent/Explore gives its last segment; any other uri names no agent, rather than naming one the CLI does not have.

The SDK reads the agent when its query is built and has no way to change it on a CLI already running, so a session's first send starts its CLI on the agent its message picked, and a later message picking a different agent closes the query and starts another resumed into the same conversation, before that turn's prompt goes out. The send that switches pays a restart; a client that sends the same agent on every message, which is the usual case, pays nothing.

A session is not tied to an agent: nothing is stored per session, because the next message carries the pick again.

Credentials ​

Sessions use whatever the Claude CLI is signed in with. A client can push a token instead. Pushed tokens are held per connection and are not used for other clients' sessions.

An agent offers its models only where a credential is set up: the CLI's own sign-in, a token, a key or another provider, a key or a provider's switch in the record's own env, or an open login the record names. With none, the picker offers none, as VS Code's Claude agent offers no models where no account is set up; a login that opens or stops reads the list again.

Several logins ​

A Claude login is a connector of the adapter claude-login, which this plugin registers: one Claude account's token, kept in the host's vault. A person makes one with x-ahpd/connectors/signin, which answers claude.ai's sign-in page for a one-year token, the page claude setup-token opens. It asks the profile scope beside the inference one. When a login opens, it reads the token's profile once: the email, masked, and the plan type show on the login before any session. A session on a login whose profile answered is told CLAUDE_CODE_OAUTH_SCOPES, so its CLI reads the account and the plan limits too. They paste the code the page shows into x-ahpd/connectors/signin-finish, which makes the connector. A token claude setup-token printed is put with x-ahpd/connectors/put instead. Either needs a Pro, Max, Team or Enterprise plan. A put of the same id replaces the token, and x-ahpd/connectors/remove takes the login away.

A record names its logins by connector id, in the order a turn tries them. A put is refused when it names an id the host serves no claude-login connector for. It is refused too when the login is neither the record owner's nor the host's.

json
{ "id": "claude", "engine": "claude", "owner": "host", "name": "Claude Code", "config": { "logins": ["claude-first", "claude-second"] } }

A turn uses the logins as the model proxy uses the entries of a name:

  • A turn runs on the first login in the record's order that is open, that the record's owner may use, and that is not resting. The CLI gets its token as CLAUDE_CODE_OAUTH_TOKEN, laid over the record's env, in a machine too. A cloud provider's switch, ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY stay out of that CLI's environment, because the CLI takes each before the token.
  • A record that names no login runs on the agent's own credential: the CLI's sign-in, the record's env or the daemon's environment. A session a client pushed a key for runs on that key and on no login. A turn whose record names logins, none of them open, is refused, and the refusal names each one, as the proxy's 503 does.
  • A request that fails with a 429 or a 5xx goes to the next login the turn has not tried. This happens only while the turn has shown nothing. The CLI says these as server_error, rate_limit and overloaded, once its own retries on that login are spent. A server_error or overloaded the API gave no answer for, a connection that never arrived, timed out or was lost, ends the turn as any other error: the provider may have run it, and the model proxy sends such a call to no other entry. A new CLI resumes the conversation at the end of the turn before it, as a truncation does, and the prompt goes out again. The failure is not shown. The steering messages sent into the turn go out behind it, so the turn still answers them, and a turn opened for a steering message sends that message as its prompt.
  • A conversation that failed on its first turn has no turn to go back to. One that this process started on this host starts over under its id, as a chat truncated whole does. One resumed, or one in a machine, ends the turn with the failure. So does one whose conversation cannot be deleted, which comes back as it was. A steering message the CLI had not taken in goes to the next login after the turn fails, and is answered in a turn of its own.
  • A failure after the turn showed something ends the turn with it, and nothing runs again. So does a failure after a turn read back from a transcript, which has no end to cut back to.
  • A refused credential, a 401 or a 403, ends the turn with Claude refused login <id> (<kind>), and no other login is tried. The CLI says it as authentication_failed, oauth_org_not_allowed or verification_required. A person signs the login in again.
  • A failure that names a retry time keeps that login out of every session until then, as a client backs off by a retry-after. A rejected rate_limit_event names it, and the CLI's own words end with resets <time>. A put of the login brings it back at once.
  • Nothing else is kept between turns: a login that failed with no retry time is tried again at the next turn.
  • When no login is left to try, the turn ends with the last failure and the first login back, with when. A turn that starts with every open login resting ends at once the same way.
  • Any other error ends the turn as it always did, with the CLI's words. So do billing_error, a 402, and account_on_hold, which is no single status. The API's errors give each status.

Each failure of a login goes to the daemon's log with the CLI's words, and each call of a login, failed or not, is noted on its connector's health, as the model proxy notes an attempt, so its row shows it. The plugin notes as a client of its own host. On a host with a users directory, its configuration entry needs the grants connector:put and connector:host to note the host's logins. A person's login is never the plugin's to note. A login's check, which counts one prompt's tokens with its token and costs nothing, runs where the host runs a connector's check: at a put, at start and at a client's command.

Rests are kept in memory, so a restart clears them, and the next failure names the time again. The CLI keeps its conversations in one configuration folder for every login, so a login resumes what another one wrote. A refused token does not fall back to a sign-in kept in that folder.

In a machine ​

A session in a machine runs the CLI from the claude part at its pinned version, not this host's: computerCli: "host" mounts this host's binary instead, and computerCliFallback: "host" mounts it only when the part cannot be built. Its configuration is a state volume at /ahpd/<id>, seeded from this host's settings, instructions, skills, agents, commands and the MCP servers of ~/.claude.json, never the sign-in. A profile with state: "host" mounts this host's ~/.claude instead, sign-in included.

The CLI there runs with the record's own env, a pushed token and CLAUDE_CONFIG_DIR, and nothing of the daemon's environment: the daemon's ANTHROPIC_* and CLAUDE_CODE_OAUTH_TOKEN cross only when the record names them with { "fromEnv" }. So a record signs in with CLAUDE_CODE_OAUTH_TOKEN, from claude setup-token, or ANTHROPIC_API_KEY in its env, and a machine that used to share this host's sign-in needs one of those or a sign-in made once inside it. See Claude Code in a machine.

Documentation ​

PLUGINS.mdLoading a plugin into the daemon
AGENT.mdThe Agent and Session contracts this implements
AHP.mdWhich actions are served, which are refused, and why

License ​

MIT © Softov