Appearance
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 host | the thing a connection is accepted by, and the name for everything below it |
| the root resource | the channel ahp-root://, which every client subscribes to |
| root state | the snapshot that channel answers: agents, activeSessions, terminals and config |
| root config | the keys a client pushes to ahp-root:// with root/configChanged |
| a scheme | the 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:
| Field | What it says |
|---|---|
agents | Every backend, as providers - see below |
activeSessions | A number: how many sessions this host is running, not a list |
terminals | Every open terminal's TerminalInfo, absent when none is open |
config | The 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:
| Field | What it says |
|---|---|
provider | The id a client names in createSession, and the scheme its session URIs use |
displayName, description | What a person reads instead of the id |
models | What 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" |
protectedResources | The 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 |
customizations | The 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 |
capabilities | multipleChats, 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.
| Key | Values | Default | What changes |
|---|---|---|---|
port | integer | 9187; 0 picks a free one | The port the host listens on |
host | address | 127.0.0.1 | The address it binds. 0.0.0.0 accepts from other machines, and then a token is required |
origins | list of origins, such as http://203.0.113.5:9187 | none | Extra 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 |
paths | list of directories | the directory the daemon was started in | The directories this host catalogues |
worktreesRoot | directory | <repo>.worktrees beside each repository | Where session worktrees are kept |
clientToolTimeoutMs | integer, milliseconds | 600000 (ten minutes) | How long a tool call a client runs may wait for that client. 0 waits for ever |
shellEnv | an object of variable name to text or null | none | The environment every shell this host starts is given. A client that needs a variable in one session sets shellInitScripts instead |
deltaWindowMs | integer, 0 to 1000 | 75 | How long a turn's streamed text is gathered before it is sent. 0 sends every delta as it arrives |
sleepAfterMinutes | integer, minutes | 30; 0 never | How long a session with nothing in hand stays loaded before its agent is let go. It wakes when something needs it - SESSIONS.md |
sessionResidencyLimit | integer | 10; 0 sets no limit | How many sessions are loaded at most. Over it, the least recently used quiet session is let go first |
advancedTools | boolean | false | Offer the tools that declare advancedPermission to every session's model |
sessionTools | boolean | true | Whether 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 |
sessionToolsProviders | list of provider ids | none | Offer 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 |
closedChats | hidden or delete | hidden | What closing one chat of a session does to that chat's conversation in the backend: hidden keeps it, delete removes it |
archiveDirtyWorktree | keep or commit | keep | What 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 |
pullRequestLookup | url or branch | url | How 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 |
updateCheck | boolean | true | Ask npm, in the background, whether a newer version exists |
wire | file path | unset | Append every frame, both directions, to that file as JSON lines |
sessions | file or memory | file | Where what this host keeps about a session is written - SESSIONS.md |
automations | file or memory | file | Whether automations survive a restart, and whether their schedules fire - AUTOMATIONS.md |
unownedAutomations | every or none | every | What an automation that names no owner wakes on - AUTOMATIONS.md |
usage | { "per": "turn" | "report", "timezone": "<zone>" } | per: turn, the system's own zone | How a turn is written down, and where a day and a week are cut - USAGE.md |
policies | { "check": true | false } | false | Whether the rows saying who may use which agent, model and computer are enforced - POLICY.md |
http | true, or { "port": <n>, "host": "<addr>" } | off | Serve the commands over HTTP under /api |
routes | { "port": <n>, "host": "<addr>" } | the daemon's port | Serve the plugins' routes on a listener of their own - PLUGINS.md |
mcpServers | an object of server entries | none | The MCP servers every session is offered |
plugins | a list of specs | none | The plugins loaded at startup - PLUGINS.md |
proxy | see PROXY.md | none | The 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.
| Key | Values | Default | What it changes |
|---|---|---|---|
defaultShell | absolute path | the system shell | The shell a host-managed terminal opens |
workspaceTrust | { "enabled": bool, "trustedUris": [uri] } | none | Which 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,wireandmcpServersapply to this daemon as written.advancedToolschanges 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;mcpServersis read again for the next session opened, while a running session keeps the servers it started with.closedChatsis read when the next chat is closed, so no session is rebuilt and none is told.archiveDirtyWorktreeis read when the next archived session gives its tree back.pullRequestLookupis read by the next pass over merged sessions.- Every other key is written to
config.jsonand the answer putsahpd.restartNeededin the_metaof the root state, which every reader of root is shown whether or not it may see the keys the notice is about.ahpd restartapplies 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.
| Command | What it does |
|---|---|
ahpd config | Print every file it read, then each key and its value, each naming the file that set it |
ahpd configure | Ask at the terminal for each setting a first install needs, and write them |
ahpd status | Say whether a daemon is running, and where |
ahpd restart | Stop it and start it again with the same line, which is what applies a change that is not live |
Grants
| What | Grant | Where it is asked |
|---|---|---|
| Read the daemon's own settings in root state | config:read | seesConfig, and the status and plugin list commands |
| Change a host-wide root config key | config:change | root/configChanged, and a replace - config:write covers it |
Push workspaceTrust | trust:push | The same action, when every key it carries is a connection's own - trust:write covers it |
Push only defaultShell | none, but a sign-in | It changes nothing anybody else reads |
| Read the account an agent bills to | account:read | seesAccount, on the root state a connection subscribes to and on every root/configChanged after it |
| Stop the daemon | config:change | The shutdown command |
| Edit the configuration over HTTP | config:write | GET /api/config, and the plugin verbs that write - the deployment token only, for those |
See also
| REFERENCE.md | The process: its verbs and its flags |
| CONFIGURE.md | How the daemon reads its configuration |
| AHP.md | The root channel's actions and their origins |
| AUTHENTICATION.md | The door, the tokens and the seven keys about who may connect |
| RESOURCES.md | The schemes this host serves and who may read them |
| PLUGINS.md | What a plugin contributes to a host |