Skip to content

The host ​

A host is what a client connects to: one root resource, the sessions running under it, the automations it holds and the resources it serves. One ahpd process runs one host, reachable over a WebSocket, over stdin and stdout inside a container, or over an HTTP API.

The process, its verbs and its flags are REFERENCE.md. This page is the host itself: what it announces, what it serves, and every setting it reads.

Terms, one line each:

Term
the hostthe thing a connection is accepted by, and the name for everything below it
the root resourcethe channel ahp-root://, which every client subscribes to
root statethe snapshot that channel answers: agents, activeSessions, terminals and config
root configthe keys a client pushes to ahp-root:// with root/configChanged
a schemethe part of a resource URI before the colon - file, user, usage, or a plugin's
a grant<subject>:<operation>, one act on one kind of thing - USERS.md

The root resource ​

ahp-root:// is the channel a client subscribes to before it knows anything else. Its state says what this host is:

FieldWhat it says
agentsEvery backend, as providers - see below
activeSessionsA number: how many sessions this host is running, not a list
terminalsEvery open terminal's TerminalInfo, absent when none is open
configThe schema and the values of root config - see Root config

_meta carries three things beside them: ahpd.resourceProviders, the schemes this host serves; ahpd.grants, every subject a grant may name; and ahpd.restartNeeded, present only when a configuration change is waiting for a restart. ahpd.principal is there for a connection that has already signed in, saying who it is - the same statement the handshake makes, so a client that subscribes later reads what a client that connected earlier was told.

ahpd.resourceProviders is advertised in the handshake as well as in every root state, because a client needs it before it can ask for anything.

The root channel is also where a client's own reads are served, whatever they are about: listSessions, the resource calls, authenticate, createResourceWatch and resolveSessionConfig all declare ahp-root:// as their channel. The four root actions and their origins are AHP.md.

What a host announces ​

Every entry of agents is one backend, as the protocol's AgentInfo:

FieldWhat it says
providerThe id a client names in createSession, and the scheme its session URIs use
displayName, descriptionWhat a person reads instead of the id
modelsWhat the backend's probe found, each row carrying its own provider because the wire type requires one. A backend that has not answered yet announces none, which is the honest form of "nobody has signed in"
protectedResourcesThe resources a client may send authenticate a token for. Absent when there are none, which means no credential can be handed to this backend at all
customizationsThe skills, subagents and MCP servers a session of this backend starts with, so a client can show what a harness offers before any session exists
capabilitiesmultipleChats, with fork and sideChat where the backend can resume at a turn; and multipleWorkingDirectories, with immutablePrimary and primaryReplacement where it takes more than one directory

ahpd.grants is the other half of what a client is told: one entry per subject a grant may name, each with its title, a one-line description, its operations and the two groups those fall into. Eleven subjects are the host's own - session, chat, file, automation, terminal, diagnostics, container, config, trust, proxy and account, plus the people and cost schemes - and every scheme a provider serves is beside them under its own name. A client draws a role editor from this one map, which is why it is advertised with or without a user directory: it says what a role could hold, not what anybody holds.

What a host serves ​

file: is the host's own scheme. Every other scheme in ahpd.resourceProviders is a provider's: user:, team:, project: and role: when a directory is configured, policy: and usage: always, and whatever a plugin registered. Each entry carries the provider's own describe(), the root URI - <scheme>:// - and the operations its methods implement, read off the provider rather than off a list somebody maintains.

A scheme is advertised so a client can put its operations in a role and have the gate ask for what it will actually call. What each scheme is and who may read it is RESOURCES.md.

Configuration keys ​

Every flag can be a key in the configuration file instead, spelled without the dashes. How the files are found, merged and checked is CONFIGURE.md; this is what the keys are.

KeyValuesDefaultWhat changes
portinteger9187; 0 picks a free oneThe port the host listens on
hostaddress127.0.0.1The address it binds. 0.0.0.0 accepts from other machines, and then a token is required
originslist of origins, such as http://203.0.113.5:9187noneExtra origins a browser may reach this host by. A host bound to 0.0.0.0 answers only to its loopback names, so a page served on its public address needs that address here
pathslist of directoriesthe directory the daemon was started inThe directories this host catalogues
worktreesRootdirectory<repo>.worktrees beside each repositoryWhere session worktrees are kept
clientToolTimeoutMsinteger, milliseconds600000 (ten minutes)How long a tool call a client runs may wait for that client. 0 waits for ever
shellEnvan object of variable name to text or nullnoneThe environment every shell this host starts is given. A client that needs a variable in one session sets shellInitScripts instead
deltaWindowMsinteger, 0 to 100075How long a turn's streamed text is gathered before it is sent. 0 sends every delta as it arrives
sleepAfterMinutesinteger, minutes30; 0 neverHow long a session with nothing in hand stays loaded before its agent is let go. It wakes when something needs it - SESSIONS.md
sessionResidencyLimitinteger10; 0 sets no limitHow many sessions are loaded at most. Over it, the least recently used quiet session is let go first
advancedToolsbooleanfalseOffer the tools that declare advancedPermission to every session's model
sessionToolsbooleantrueWhether the session tools - the nine that read and move the sessions beside this one - are offered at all. false offers them to no session. The artifact tools and schedule_wakeup are not session tools and are offered either way
sessionToolsProviderslist of provider idsnoneOffer the session tools to the sessions of these providers alone. Empty, the default, offers them to every provider. It narrows sessionTools rather than replacing it, so with sessionTools false no session is offered them whatever this list holds
closedChatshidden or deletehiddenWhat closing one chat of a session does to that chat's conversation in the backend: hidden keeps it, delete removes it
archiveDirtyWorktreekeep or commitkeepWhat archiving a session does with a working tree that has uncommitted changes: keep leaves the tree, commit commits the changes on the session's branch with the message "Saving uncommitted changes before archiving session", then removes the tree and keeps the branch
pullRequestLookupurl or branchurlHow the pass that archives and deletes merged sessions learns the state of a pull request: url asks GitHub for each related pull request by its page, branch asks for the pull requests of the session's branch
updateCheckbooleantrueAsk npm, in the background, whether a newer version exists
wirefile pathunsetAppend every frame, both directions, to that file as JSON lines
sessionsfile or memoryfileWhere what this host keeps about a session is written - SESSIONS.md
automationsfile or memoryfileWhether automations survive a restart, and whether their schedules fire - AUTOMATIONS.md
unownedAutomationsevery or noneeveryWhat an automation that names no owner wakes on - AUTOMATIONS.md
usage{ "per": "turn" | "report", "timezone": "<zone>" }per: turn, the system's own zoneHow a turn is written down, and where a day and a week are cut - USAGE.md
policies{ "check": true | false }falseWhether the rows saying who may use which agent, model and computer are enforced - POLICY.md
httptrue, or { "port": <n>, "host": "<addr>" }offServe the commands over HTTP under /api
routes{ "port": <n>, "host": "<addr>" }the daemon's portServe the plugins' routes on a listener of their own - PLUGINS.md
mcpServersan object of server entriesnoneThe MCP servers every session is offered
pluginsa list of specsnoneThe plugins loaded at startup - PLUGINS.md
proxysee PROXY.mdnoneThe providers this host's model proxy calls, and the model names that point at them

Seven keys are about who may connect rather than about how the host behaves - connectionToken, connectionTokenFile, withoutConnectionToken, users, resource, issuer and trustToken - and they are AUTHENTICATION.md. stdio, configFile, noPlugins, noCwd and pluginOptions mean something only when typed, so a file that sets one is warned about and ignored.

Each key that needs more than a line:

paths is the list of directories this host catalogues, and the first is where a session goes when a client names none. A relative one is taken from the directory of the file that set it, not from where the daemon was started. --no-cwd serves only what is named and asks about no other folder; it is refused when the list would be empty. worktreesRoot keeps every session worktree under <dir>/<repo>/<name> instead of the reference host's <repo>.worktrees beside each repository, and is made absolute against the working directory because it is a base other paths are joined to.

shellEnv is the environment every shell this host starts is given, as a map of variable name to value. A daemon that runs as a service unit has an environment of its own, and this is how the operator says which of it an agent's shells get: {"shellEnv": {"XDG_CONFIG_HOME": "/home/me/.config"}} opens every terminal and runs every Bash command with that directory. A name set to null is removed from what the daemon itself runs with, which is the other half - a shell that inherits a variable the operator does not want it to have. It reaches the host terminals and the Claude backend's Bash commands, and the other backends come later. A wrong value is ...: shellEnv.PORT must be text or null, and a name a shell cannot export is ...: shellEnv.2FA is not a shell variable name. A client that needs a variable in one session sends shellInitScripts, which is that session's own.

deltaWindowMs is how long the host gathers the streamed text of one part before sending it, in milliseconds: within that window a turn's deltas are merged into one action, so a client draws the same text from fewer envelopes. A wrong value is ...: deltaWindowMs must be an integer between 0 and 1000.

wire appends every frame, both directions, to that file as JSON lines, one message per line with an _ahpLog beside it - the shape VS Code's agent host writes its traffic log in. A line over 1 MiB is written again with its strings cut and _ahpLog.truncated set; a file over 75 MiB rolls to <file>.1 and five files are kept. The capture holds every token a client sent in authenticate, so each of its files is 0600. pnpm wire -- <file> checks one against the schema.

usage says how a turn is written down and where the periods are cut. per is turn, the default, which holds what a turn has used and writes one record when the turn ends, or report, which writes one record for every usage report so a turn still running is already billed for what it has spent. Both bill the same work, and a wrong value is ...: usage.per must be one of turn, report. timezone names, as Intl names one, the zone a day starts at and a week starts on - a week is Monday 00:00 there, not the system's own - and the system's own zone is used when the key is absent or names a zone this host cannot read, which is said once at start.

The same store is served as the usage: scheme, so a client reads it through the resource calls it already has: usage:// lists the charged pools that reader may read, usage://<pool> reads that pool as { pool, kind, name, day, week, month }, usage://<pool>/range?from=&until= reads one total between two days, usage://<pool>/records?from=&until= lists the records charged to it, newest first, at most 200, and usage://groups?by=&from=&until= sums the records by user, team and project (USAGE.md). A pool name holds colons, so it is one encoded path segment - usage://project%3Abackend%3Asearch is project:backend:search, not an authority. ahpd usage with no pool lists what that caller may see, and with one prints that pool's three totals, cut in usage.timezone.

policies.check says whether the rows saying who may use which agent, model and computer are enforced. Off by default. The rows are kept and the policy: scheme is served either way, so a store can be filled in before anything is switched on; a wrong value is ...: policies.check must be true or false.

http serves the same commands the terminal renders, under /api. true puts it on the host's own listener; an object with a port gives it a listener of its own, and its host binds that listener - decision The HTTP API is off by default, and on the daemon's own port under /api unless http.port is set, and http.host binds the API's own listener. A host with neither a connection token nor a user directory refuses to start with it on, because there would be nothing to hold a request against.

mcpServers is the host's own MCP servers, by the name a person gave each, and every session is offered them whatever agent it runs. An entry is one of the two shapes VS Code uses: a stdio server needs a command, and an http one a url:

json
"mcpServers": {
  "files": { "type": "stdio", "command": "mcp-server-filesystem", "args": ["/srv"], "cwd": "/srv", "env": { "TOKEN": "..." } },
  "issues": { "type": "http", "url": "http://127.0.0.1:9310/mcp", "headers": { "Authorization": "Bearer ..." } }
}

An entry that is neither shape, or that is missing the one its own type needs, is one warning in the log and is left out: one server nobody can reach is a gap in one agent's reach, where a wrong port is a daemon that would not run at all. A value in env or in headers is a credential, so each answers <set> here and over the API alike.

plugins names what is loaded at startup: a package, a path, or a package installed in the configuration directory. Naming one runs its code in this process with this process's permissions, which is why installing a plugin is the trust decision. A plugin is loaded once and its options make its variants, so an entry may be an object naming one with its options and enabled - PLUGINS.md.

Root config ​

Root config is not a file. It is the record a client pushes to ahp-root:// with root/configChanged, kept per host, and read back on every root state. Three kinds of key live in it. The host's own are the ones this host acts on. The settings VS Code's agent host declares are declared here in full and read back, though only globalAutoApproveEnabled changes what this host does. The daemon's are the keys of config.json a client may edit.

The host's own schema is two keys, and it is what a client draws its controls from. A key in the schema is a promise that pushing it changes something.

KeyValuesDefaultWhat it changes
defaultShellabsolute paththe system shellThe shell a host-managed terminal opens
workspaceTrust{ "enabled": bool, "trustedUris": [uri] }noneWhich folders the window that pushed it trusts. readOnly: the window pushes it, it is not a control

Beside them the schema declares every key VS Code's agent host declares at 7516b04bc94. They come from packages/sdk/src/vscoderootconfig.ts, each with the property VS Code gives it: telemetryLevel, globalAutoApproveEnabled, showExternalSessions, agentMerge.* and the rest. Only one of those is read here: globalAutoApproveEnabled, which trust.ts asks before a tool call runs. The rest are declared and nothing more. A client draws its settings pane from the schema, so a key it cannot find is a control it cannot draw.

Three keys VS Code declared before 1.140 are not declared: artifactToolsCompactPrompts, deferredTitleGeneration and activeAgentTitleGeneration. The artifact tools carry one wording, and every session runs under the deferred title strategy.

defaultShell and workspaceTrust are per connection: they are kept on the connection that pushed them, and a connection reads its own back rather than whatever was pushed last. The rest are one setting for the whole host. The declaration is decision The root config declares every key VS Code pushes, as the reference declares it, and refuses a key nobody declares. The refusal of a key nobody declared is decided there too.

One value is in config.values and in no schema at all: the account each agent's harness is signed in as, under vscode.codexAccount for Codex - the key VS Code's window reads the account of its own host's Codex agent from - and ahpd.<provider>Account for every other backend. Nobody declares it and so no client can push one, which is the point: the host writes it when a probe answers with an account or a running session reports one, and a client that writes to the key is refused like any other key nobody declared. The email in it is the operator's own, so a connection that is not root and does not hold account:read is shown no account value at all, in the state it subscribes to and in every change after it.

The daemon's half is added to the same schema, and config:read is what a connection needs to be shown it. It is paths, port, host, http, routes, updateCheck, advancedTools, sessionTools, sessionToolsProviders, closedChats, archiveDirtyWorktree, pullRequestLookup, wire and mcpServers, and each configured plugin is one more key, plugins.<id> by its id, whose value is { enabled, options } - so one plugin is one key and its variants are its own options. Nothing else the file holds is there: stdio, configFile, noCwd, worktreesRoot, clientToolTimeoutMs, the connection token keys, trustToken, issuer, resource, users, automations and sessions are still edited the way they always were.

Pushing a key acts in one of two ways, and the answer says which.

  • advancedTools, sessionTools, sessionToolsProviders, closedChats, archiveDirtyWorktree, pullRequestLookup, wire and mcpServers apply to this daemon as written. advancedTools changes the tools every running session's model is offered at once; the two session tool keys are rebuilt per session, since a provider list is not the same answer for all of them, and each running session is told its own set; the wire capture starts, moves or stops; mcpServers is read again for the next session opened, while a running session keeps the servers it started with. closedChats is read when the next chat is closed, so no session is rebuilt and none is told. archiveDirtyWorktree is read when the next archived session gives its tree back. pullRequestLookup is read by the next pass over merged sessions.
  • Every other key is written to config.json and the answer puts ahpd.restartNeeded in the _meta of the root state, which every reader of root is shown whether or not it may see the keys the notice is about. ahpd restart applies it.

That split, and the reason it is per key rather than all-or-nothing, is decision A configuration change applies live where the key can, and otherwise on ahpd restart.

A key shows what the file holds rather than what this run is using, and when a start flag overrode it the key's description says so. A credential is never sent back: every value a plugin's own options schema marks writeOnly, however deep in its options the mark sits, is answered as <set>, here and over the API alike, and a client that sends that back has said the credential is left as it is.

Commands ​

The verbs that read or change this host's own settings, one line each. The rest are REFERENCE.md.

CommandWhat it does
ahpd configPrint every file it read, then each key and its value, each naming the file that set it
ahpd configureAsk at the terminal for each setting a first install needs, and write them
ahpd statusSay whether a daemon is running, and where
ahpd restartStop it and start it again with the same line, which is what applies a change that is not live

Grants ​

WhatGrantWhere it is asked
Read the daemon's own settings in root stateconfig:readseesConfig, and the status and plugin list commands
Change a host-wide root config keyconfig:changeroot/configChanged, and a replace - config:write covers it
Push workspaceTrusttrust:pushThe same action, when every key it carries is a connection's own - trust:write covers it
Push only defaultShellnone, but a sign-inIt changes nothing anybody else reads
Read the account an agent bills toaccount:readseesAccount, on the root state a connection subscribes to and on every root/configChanged after it
Stop the daemonconfig:changeThe shutdown command
Edit the configuration over HTTPconfig:writeGET /api/config, and the plugin verbs that write - the deployment token only, for those

See also ​

REFERENCE.mdThe process: its verbs and its flags
CONFIGURE.mdHow the daemon reads its configuration
AHP.mdThe root channel's actions and their origins
AUTHENTICATION.mdThe door, the tokens and the seven keys about who may connect
RESOURCES.mdThe schemes this host serves and who may read them
PLUGINS.mdWhat a plugin contributes to a host