Agent Provisioning
agentctl create --agent=<name> just-in-time installs a coding agent into a
freshly created sandbox, instead of you hand-typing its install command
after shell-ing in.
$ agentctl create demo --image=images:ubuntu/24.04 --agent=claude
agentctl: installing claude...
...
$ agentctl shell demo
$ claude --version
What --agent does
- Looks up
<name>in agentctl's built-in registry (see below). An unrecognized name fails immediately, before anything is created, and lists the valid names. - Merges that agent's required egress allow-domains into the instance's
network policy, so the install and the agent's own runtime API calls
aren't blocked by the default-deny egress policy — no separate
--allowneeded for the common case. - Sets the instance's default non-root user
to the agent's name (e.g.
--agent=claudecreates and runs as aclaudeuser), unless you're already relying on a different default elsewhere. - Because installing requires a running, agent-ready instance while
createnormally leaves the instance stopped,--agentimpliesstart— you get back a running instance, not a stopped one. - Runs the agent's install command as that non-root user, streaming its output to your terminal. A curl installer is downloaded to a file and run only if the download succeeded, so a blocked download fails the install instead of being recorded as one.
Built-in registry
--agent value |
Installs | Host API key it expects | Egress allowed (port 443) |
|---|---|---|---|
claude |
Claude Code | ANTHROPIC_API_KEY |
claude.ai, downloads.claude.ai, platform.claude.com, api.anthropic.com, statsig.anthropic.com |
codex |
OpenAI Codex CLI | OPENAI_API_KEY |
chatgpt.com, releases.openai.com, auth.openai.com, api.openai.com |
cursor |
Cursor CLI (cursor-agent) |
CURSOR_API_KEY |
cursor.com, api2.cursor.sh |
gemini |
Gemini CLI | GEMINI_API_KEY |
registry.npmjs.org, generativelanguage.googleapis.com, accounts.google.com, oauth2.googleapis.com |
opencode |
opencode | (multi-provider — none) | opencode.ai, github.com, api.github.com, release-assets.githubusercontent.com, models.opencode.ai |
pi |
pi | ANTHROPIC_API_KEY (default provider) |
pi.dev, registry.npmjs.org, nodejs.org, api.anthropic.com |
Each list covers what the installer itself fetches — install scripts typically download the actual program from a different host, which is why there's more here than one domain per agent — plus what the agent needs to sign in and talk to its default model provider:
- claude:
claude.ai/install.shdownloads Claude Code fromdownloads.claude.ai(also where its auto-updates come from). Signing in with either a claude.ai or a Console account exchanges and refreshes its OAuth token withplatform.claude.com; the API itself isapi.anthropic.com. Source: Claude Code's network access requirements. - codex: the installer downloads from
releases.openai.com; signing in with ChatGPT goes throughauth.openai.com, ChatGPT-plan usage throughchatgpt.com, API-key usage throughapi.openai.com. - cursor: the install script comes from
cursor.com, andapi2.cursor.shcarries most API requests (see the note below). - gemini: installs from npm (
registry.npmjs.org); the Gemini API isgenerativelanguage.googleapis.com, and Google sign-in usesaccounts.google.comandoauth2.googleapis.com. - opencode: the installer looks the latest release up on
api.github.comand downloads it fromgithub.com, which redirects torelease-assets.githubusercontent.com; opencode fetches its model catalog frommodels.opencode.aiat startup. - pi: the installer installs pi from the npm registry and needs
Node.js 22.19.0 or newer. It only offers to install Node itself when it
has a terminal, which it never has under agentctl, so agentctl provides
Node first. If
node -valready reports 22.19.0 or newer, nothing changes. Otherwise, on a glibc guest (Ubuntu, Debian, Fedora) agentctl installs the latest Node 22 LTS release fromnodejs.org/dist/latest-v22.x, verifies it against that release'sSHASUMS256.txtand unpacks it into/usr/local; on Alpine it runsapk add nodejs npmand fails with the version it found if that is older than 22.19.0. Without passwordless sudo, the nodejs.org build goes into~/.local/nodeinstead, and a line putting~/.local/node/binonPATHis added to~/.profile. Then pi's installer runs unchanged, and agentctl puts pi's~/.pi/agent/binonPATHthe same way, via~/.profile, soagentctl shellfindspiby name. A failure prints a line startingagentctl: pi:, andagentctl start <name>retries the install. pi also wants ripgrep and fd, and downloads them fromgithub.comat first use if they are missing, which the egress policy refuses; so before pi's installer agentctl installs them from the distribution (apt-get,apkordnf) when the instance allows that package source (--allow-preset apton Debian/Ubuntu,--allow-preset apkon Alpine), and otherwise prints oneagentctl: pi:line saying so and carries on, leaving pi without them.
Every host is named outright, never as a wildcard: Lima's egress proxy
would match a *.example.com entry against each hostname, but Incus
resolves only the wildcard's apex domain (see
Profiles & Policies), so a
wildcard would make the same agent's allowlist mean different things on
the two backends.
opencode and pi are explicitly multi-provider tools: opencode connects
to whichever model backend you configure inside the sandbox, and pi
defaults to Anthropic but also supports others. Their table entries above
only guarantee installing them (pi additionally covers its
default provider) — if you point either at a different
model provider, extend the allowlist the same way you would for anything
else: agentctl create demo --agent=opencode --allow=<your-provider-domain>:443.
--agent only opens what the agent needs. If it will also install
packages or clone repositories, allow those sources too — e.g.
--allow-preset=github,pypi (see
Profiles & Policies).
Allowing an agent without --agent
Each agent's hosts are also an allow preset of the same name, so
--allow-preset=claude (or allowPresets: [claude] in a profile) allows
exactly what --agent=claude does without installing anything — for an
image that already has the agent, installing it some other way, or an
org profile meant for Claude Code work:
$ agentctl create demo --image=images:ubuntu/24.04 --allow-preset=claude,apt,github
$ agentctl start demo
$ agentctl exec demo -- sh -c 'curl -fsSL https://claude.ai/install.sh | bash'
Tools the agent needs, that the image doesn't have
--agent installs the agent and nothing else. Base images are minimal —
images:ubuntu/24.04 has no git, no compiler, no editor — so an agent
that can run may still be unable to do much.
Install them in the same create, with --package or a profile that
bundles them:
$ agentctl create demo --image=images:fedora/44 --profile=terminal --agent=claude
Packages are installed before the agent, so an agent whose installer
needs a tool present (--agent=gemini needs npm) can get it the same
way:
$ agentctl create demo --image=images:ubuntu/24.04 --package=nodejs --package=npm --agent=gemini
See Tools in the guest.
gemini needs Node.js already in the guest
Google documents no curl-based installer for Gemini CLI — only npm, npx and
Homebrew — so --agent=gemini runs npm install -g @google/gemini-cli and
requires Node.js 20+ to already be present in the image. None of the base
images used throughout these docs (images:ubuntu/24.04, images:alpine/edge,
template://ubuntu-lts) ship Node, so on a bare image this fails with
npm: command not found rather than installing anything.
agentctl deliberately does not bootstrap Node for you: that would be inventing
an install path the agent's own documentation doesn't describe. Ask for it
explicitly instead — --package=nodejs --package=npm on the same create
(installed before the agent, so the install finds it), or use an image that
already has it. If you've already hit the failure, install into the existing
instance — agentctl exec --root demo -- apt-get install -y nodejs npm —
and agentctl start demo retries the pending install automatically (see
below).
cursor may need more domains than the table lists
Cursor spreads runtime traffic across more hosts than the other agents. The
entry above covers install plus the primary API path (api2.cursor.sh), but
Cursor's own network-configuration docs also name api3/api4/api5,
repo42.cursor.sh, the *.authentication.cursor.sh hosts and
*.cursor-cdn.com as needed in some setups. If something fails to connect,
extend the allowlist the same way as for a multi-provider agent, naming
the hosts — agentctl create demo --agent=cursor --allow=api3.cursor.sh:443.
(A wildcard such as --allow='*.cursor.sh:443' covers them all on Lima,
but on Incus it only allows cursor.sh itself.)
What it doesn't do (yet)
Forwarding a host API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, ...) into
the guest is a separate, not-yet-built mechanism — --agent installs the
agent binary, it doesn't configure credentials for you. Set the key inside
the sandbox the same way you would on any machine (an env var, a config
file, whatever the agent's own docs say) after shell-ing in.
If install fails partway
A transient failure during install (e.g. a flaky network mid-curl) isn't
silently swallowed: create --agent=<name> returns a real error, and the
instance is left running with the failure recorded. The next
agentctl start <name> (even a completely ordinary one, with no --agent
flag) detects the incomplete install and retries it automatically before
doing anything else — see
Troubleshooting
for details. A plain create (no --agent) is entirely unaffected by any
of this.
Where that "was an install requested, did it finish" state actually lives
differs per provider: on Incus it's recorded on the instance itself (via
incus config get <name> user.agentctl-agent), so the backend's own
per-instance config stays the single source of truth. Lima has no
equivalent per-instance custom-metadata primitive, so on Lima this state
lives in a small local file instead, ~/.config/agentctl/lima-state.yaml
(or next to whatever AGENTCTL_CONFIG points at) — see
internal/provider/lima/state.go. This only matters if you're inspecting
or scripting around this state directly; agentctl start's retry
behavior above is identical either way.