Troubleshooting
"not available" / "under development" / manual workaround — what do these mean?
agentctl checks whether the active provider actually supports what you
asked for before doing anything, and always tells you one of four things:
- (nothing printed, command just runs) — supported, proceeding normally.
- "is under development for provider ..." (exit code
2) — agentctl knows how to build this, just hasn't yet. Not a platform limitation. Nothing to do but wait for it or use a different provider if one supports it. - "is not available on provider ..." (exit code
2) — the platform genuinely has no way to do this, and no workaround is known. - "has no native support on provider ..." followed by a numbered plan
(exit code
3) — a manual procedure exists. Apply it by hand, then either re-run with--force-partialto proceed anyway (accepting that agentctl itself isn't enforcing this), or just run the manual steps and use the underlying tool directly for this one operation.
See the full Capability Matrix for what's supported on each provider today.
no provider configured
Run agentctl config set provider=<incus|lima|hyperv>, or pass
--provider=<name> for a one-off override.
Lima: create fails with "is not a Lima image reference"
The Lima backend has no image store: --image (or a spec's image:) must be
a Lima template reference, a URL, or a local template path, not an Incus
reference such as images:ubuntu/24.04. agentctl refuses the Incus form before
doing anything, and suggests the matching template where it can:
$ agentctl create demo --image=images:ubuntu/24.04
agentctl: image "images:ubuntu/24.04" is not a Lima image reference: ... did you mean --image=template://ubuntu-24.04 ? (template://ubuntu-lts is the usual choice)
Use --image=template://ubuntu-lts, or pick another name from
limactl template ls. The suggestion is a guess, so check that it is listed.
See Lima setup.
exec: "incus": executable file not found in $PATH
The Incus backend shells out to the incus CLI rather than linking a Go SDK
— install and initialize Incus first. See
Incus setup.
A LAN request I expected to succeed is being blocked
That's --deny-lan (on by default) doing its job — the sandbox can't reach
other devices on your network. If you genuinely need that, pass --allow-lan
on create, or set denyLAN: false in a profile, understanding the tradeoff.
Note that --deny-lan is enforced by not allowing the private ranges rather
than by rejecting them, because an Incus reject rule would shadow every allow
rule including DNS. One consequence worth knowing: if an --allow domain
resolves to a LAN address, that rule is silently dropped rather than applied,
since "allow this host, but not the LAN" can't be expressed in a single ACL.
--preview shows exactly which rules survive.
If the host you need is one machine — a model server, a mirror — list it by
IP address with its ports instead, --allow=10.20.30.40:8080: that one
host:port is reachable while deny-LAN stays on for everything else (see
Egress allowlist).
create fails with "allow entry … is a LAN address: list the port(s) you need"
Error: allow entry "10.20.30.40" is a LAN address: list the port(s) you need (e.g. 10.20.30.40:8080); deny-LAN permits a LAN host only on listed ports
Under deny-LAN, a private IP address on the allowlist is honoured only on
the ports it lists; "any port" on a LAN host isn't a narrow exception, so
create refuses it rather than letting it fail later. List the host:port as
an IP literal — --allow=10.20.30.40:8080 — or, if you really want the
whole LAN on any port, pass --allow-lan. A link-local (169.254.x.x,
fe80::) or loopback address is refused the same way whatever its ports:
deny-LAN never opens those.
Nothing in the sandbox can reach the network / curl fails during an agent install
Run the diagnostic first — it walks every layer below and prints which one is at fault, changing nothing:
$ scripts/diagnose-incus-network.sh <name>
The manual version, in order. The first two are host-side and far more common than a problem with the policy itself.
0. Rule agentctl out entirely. If --no-network-policy also has no
network, no ACL was ever created or attached, so the cause is below agentctl —
skip to step 1 and treat it as a plain Incus networking problem. Note that
incus exec on a VM goes over vsock rather than the network, so exec
working tells you nothing about connectivity.
1. On Fedora/RHEL, check firewalld. If the guest has an IPv6 address but no IPv4 one, firewalld is blocking DHCP on the bridge. See Incus setup.
$ incus exec <name> -- ip -br addr # IPv6 only? -> firewalld
2. Check DNS specifically, since everything else depends on it:
$ incus exec <name> -- getent hosts example.com
If that fails but the instance otherwise looks healthy, the ACL is the suspect. Confirm by detaching it:
$ incus config device unset <name> eth0 security.acls
$ incus exec <name> -- getent hosts example.com # works now? -> the ACL
3. Create a sandbox with no policy at all to get a working baseline you can compare against:
$ agentctl create debug --image=<ref> --no-network-policy
That skips ACL creation and attachment entirely. It is an explicit, visible escape hatch for exactly this situation — the sandbox gets unrestricted egress, so use it to diagnose, not as a habit.
4. Inspect what agentctl would actually apply, without applying it:
$ agentctl --preview create demo --image=<ref> --agent=claude
Every incus network acl rule add line is shown verbatim. Note that with
--deny-lan on, an --allow domain that resolves to a LAN address is
deliberately omitted rather than allowed — so a missing rule there is the
policy working, not a bug.
start fails with "incompatible with secureboot"
Error: The image used by this instance is incompatible with secureboot. Please set security.secureboot=false on the instance
This shouldn't happen on current agentctl — create disables Secure Boot
by default precisely because most public/community images aren't signed for
it (see Incus setup). If you hit this
on an instance created by an older agentctl build, fix it directly:
incus config set <name> security.secureboot=false, then agentctl start
again.
create fails during non-root user provisioning
agentctl create runs a bootstrap script inside the instance (right after
network policy is applied) to provision the non-root default user that
shell/exec use — see
Profiles & Policies. Two
ways this can fail:
- Neither
useraddnoradduseris present. Some minimal/custom images ship neither tool. The bootstrap script fails with a clear error instead of silently leaving the instance root-only. There's no workaround short of using an image that includes one of them (any mainstream Debian/Ubuntu or Alpine-family image does). sudocan't be installed. The script triesapt-get install -y sudoorapk add --no-cache sudoifsudois missing, which needs working network egress at create time. If your allowlist is unusually restrictive before the instance is fully up, this step can fail — check the instance's egress policy, or use an image that already includessudopreinstalled.
Re-running the bootstrap step is safe: it's idempotent and a no-op if the
user already exists, so retrying agentctl create (after deleting the failed
instance) or a future retry mechanism won't double-provision anything.
shell/exec say "Error: VM agent isn't currently running"
agentctl shell/agentctl exec need the in-guest incus-agent to have
connected, which normally happens within a few seconds of start but can
take longer depending on the image and hardware. shell/exec already
wait up to 30 seconds for it, printing "waiting for the VM agent... to
start" to stderr if it takes a moment — you shouldn't need to do anything
but wait.
If it still fails after 30 seconds, either the guest is taking unusually
long to boot (just retry agentctl shell/exec again), or the image
doesn't include incus-agent support at all (some minimal or custom images
don't) — in which case exec/shell won't work on that image, but
agentctl view (the console) still will, since it doesn't need the agent.
applying network policy fails with "Duplicate of egress rule N"
Error: incus network acl rule add agentctl-<name> egress action=allow destination=<ip> protocol=tcp destination_port=443: exit status 1: Error: Duplicate of egress rule 4
This shouldn't happen on current agentctl — it was a real bug where two
different allow-domains that happened to resolve to the same IP (common
behind shared CDN infrastructure; e.g. --agent=claude's claude.ai and
*.anthropic.com allow-domains can share an IP) produced two
byte-identical ACL rules, which Incus rejects on the second one. Fixed by
deduplicating identical rule commands before applying the policy. If you
still hit this on an older agentctl build, no workaround is needed beyond
upgrading — there's nothing to fix in your own profile or --allow flags.
provisioning default user: user bootstrap script exited 1: Error: Instance is not running
This shouldn't happen on current agentctl — it was a real bug where
create tried to run the non-root user bootstrap step (which needs incus
exec, and incus exec requires a running instance) against a freshly
init'd instance, which is stopped by design. Fixed by having create
briefly start the instance around the bootstrap step and stop it again
afterward — see Incus setup for what
that means for create's timing. If you still hit this on an older
agentctl build, upgrade; there's nothing to change in your own command or
profile.
Agent install fails or never finishes
create --agent=<name> runs that agent's install command (e.g. curl ... |
bash) as soon as the instance is up — see
Agent Provisioning. This needs working egress at
install time, so a flaky network mid-install, or an install script that
starts fetching from a host agentctl's registry doesn't list yet (each
agent's allowlist covers the hosts its installer used when it was last
checked — see Agent Provisioning),
can leave create reporting a failure with the instance left running.
You don't need to redo anything by hand: the very 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. If it keeps failing, the error create/start printed is the actual
install script's output; check that against whatever domain it's trying to
reach and extend --allow if it's egress being blocked, same as any other
blocked-domain issue.
An already-successful install is never repeated: create --agent=<name>
records success once the install script exits 0, and every subsequent
start is a no-op with respect to the agent.
--agent=pi fails with "No terminal detected; install Node.js 22.19.0 or newer and npm"
That message comes from pi's own installer, on agentctl builds from before agentctl started providing Node.js for pi. pi needs Node.js 22.19.0 or newer and only offers to install it when it has a terminal, which an agent install never has, so on an image without Node it stops there.
Upgrade agentctl: current builds install Node before running pi's installer
(see Agent Provisioning), and the
next agentctl start <name> retries the incomplete install. To fix an
instance without upgrading, put Node into it yourself, for example on a
glibc guest by unpacking a node-v22.*-linux-<arch>.tar.xz from
https://nodejs.org/dist/latest-v22.x/ into /usr/local (sudo tar -xJf
<file> -C /usr/local --strip-components=1), then agentctl start <name>
to retry.
pi says "fd not found. Downloading…" then "proxy response 403 !== 200 when HTTP tunneling"
fd not found. Downloading…
Failed to download fd: fetch failed… proxy response 403 !== 200 when HTTP tunneling
and the same for ripgrep. pi uses fd and ripgrep for file search and
downloads them from github.com at first use when they are not on PATH;
the sandbox's egress policy refuses that, so pi runs without them.
create --agent=pi installs both from the distribution before pi's
installer, but only when the instance can reach its package source. If
the install printed agentctl: pi: could not install ripgrep and fd,
recreate the instance with --allow-preset apt (Debian/Ubuntu) or
--allow-preset apk (Alpine). For an existing instance, install them
yourself where the package source is reachable, for example
agentctl exec --root <name> -- apt-get install -y ripgrep fd-find on
Debian/Ubuntu; pi also finds Debian's fdfind binary under that name.
Agent install fails with "could not download ... check the agent's allow preset"
agentctl: pi: could not download https://pi.dev/install.sh (curl exit 22); the sandbox's egress policy may be refusing it — check the agent's allow preset
The agent's installer could not be downloaded — most often because the
instance's egress policy refused its host. agentctl now fails the install
here; it used to pipe the download straight into a shell, which ran an
empty script, reported success and never retried. Allow the agent's
preset (--allow-preset=<agent>, which --agent=<agent> adds for you) or
the host the message names, then run agentctl start <name> to retry.
An agent installed successfully but claude/codex/... is "command not found"
Almost always a PATH problem rather than a failed install, and the fix is
already in agentctl — this section is here because instances created by an
older build still carry the symptom.
Agent installers put their binary in ~/.local/bin, and every distribution
puts that directory on PATH from a login shell profile
(/etc/profile.d/ on Fedora, the skel ~/.profile on Debian). incus exec
runs neither a login nor an interactive shell, so none of those were ever
sourced: the install genuinely succeeded and the binary genuinely wasn't
findable.
agentctl now sets PATH (and HOME/USER/LOGNAME) explicitly on every
exec, and agentctl shell opens a real login shell. Check what you're
getting:
$ agentctl exec demo -- sh -c 'echo $PATH'
/home/claude/.local/bin:/home/claude/bin:/usr/local/sbin:...
If ~/.local/bin isn't first, the instance predates the fix. Either
recreate it, or call the binary by its full path
(agentctl exec demo -- ~/.local/bin/claude).
The same non-login behavior is why a sandbox's shell could come up with no
prompt, no colors and no locale: ~/.bashrc was sourced but /etc/profile
was not.
agentctl shell has no dotfiles / an empty home directory
useradd -m copies /etc/skel into a new home directory, and busybox
adduser usually does too — "usually" being the problem, since it depends
on how busybox was compiled. agentctl's bootstrap script now copies any
missing /etc/skel entry explicitly, and never overwrites one that already
exists.
That runs on every provisioning pass, including the idempotent one for an
account that already exists, so an instance created by an older build is
repaired the next time its user is provisioned rather than staying broken.
agentctl start <name> on an instance with a pending agent install is the
usual trigger; otherwise recreate it.
apt, pip, npm or git clone can't reach its server
Egress is default-deny, so package managers and git only work against
hosts on the instance's allowlist — and they usually need more than one
host each (pip downloads from files.pythonhosted.org, not pypi.org;
GitHub release downloads come from release-assets.githubusercontent.com).
Create the instance with the matching
allow presets,
e.g. --allow-preset=apt,pypi,github, and add anything else with --allow.
The tool's own error usually names the host it couldn't reach (apt-get
update lists each failed URL); on Lima, the instance's egress proxy log
(see below)
names every refused host:port. The allowlist is set when the instance is
created, so recreate it with the extra entries. (Tools requested with
--package/packages: are different: on Incus they're installed during
create, before the network policy is attached — see
Packages —
so they don't need a preset.)
An --allow entry stopped working after a while
On Incus, domain-based allow rules are resolved to IP addresses when the
policy is applied — a snapshot, not dynamic DNS-aware filtering. If the
target rotates IPs (common behind a CDN), re-apply the policy (re-run
create's network setup, or recreate the instance) to refresh it. (On
Lima this can't happen: its egress proxy resolves the hostname on every
connection.)
Lima: a request from the sandbox gets 403 / "denied by agentctl network policy"
On Lima, the sandbox's traffic goes through a per-instance egress proxy
(see Lima setup),
which answers a refused destination with 403 Forbidden and the reason —
curl shows it as CONNECT tunnel failed, response 403 for HTTPS. Every
decision is logged on the Mac in
~/.config/agentctl/lima/<name>/egress-proxy.log, so that's the quickest
way to see exactly which host:port was refused and why:
2026-10-07T19:08:50Z deny CONNECT downloads.example.com:443: not in the allowlist
Allow rules match the hostname the client asked for, so a site that
redirects to another hostname (a download CDN, say) needs that hostname
allowlisted too. Add it with --allow and recreate the instance — and the
same reasons as on Incus apply to "deny-LAN is on" refusals.
Lima: a tool can't connect at all ("Connection refused"), even to an allowlisted host
That tool isn't using the instance's proxy. On Lima, the sandbox's only way
out is the egress proxy its http_proxy/https_proxy variables point at;
a connection that doesn't go through it is refused by design, allowlisted
destination or not (that's what stops a compromised agent from simply
ignoring the proxy). Configure the tool to use an HTTP proxy — the
https_proxy value in the sandbox's environment — or use a tool that
honors those variables. Some common cases: Node's built-in fetch and
http/https modules only honor them with NODE_USE_ENV_PROXY=1 (or
--use-env-proxy; Node 22.21+/24.5+); git over SSH needs an ssh
ProxyCommand (or use the HTTPS remote); anything UDP-based has no way out.
Lima: agentctl start refuses an instance that's "already running, but agentctl didn't start it under its network sandbox"
The instance was started some other way — a plain limactl start, for
instance — so its egress is unrestricted, and agentctl won't pretend
otherwise by adopting it. Stop it and start it with agentctl:
$ agentctl stop <name>
$ agentctl start <name>
Lima: agentctl start refuses an instance "registered with launchd to start automatically"
limactl autostart enable (formerly start-at-login) has launchd start the
instance at login — outside agentctl's sandbox, with unrestricted egress.
Unregister it with limactl autostart disable <name> and start it with
agentctl instead.
Lima: agentctl start refuses because of the instance's configuration
agentctl checks Lima's effective configuration for the instance before every start and refuses anything that would undermine its network policy:
- "has additional networks configured" — a vzNAT or socket_vmnet
network, whose traffic doesn't go through the sandboxed Lima hostagent.
agentctl removes these from the instance itself, so this means one is
coming from
~/.lima/_config/default.yamloroverride.yaml; remove it there. - "ssh.localPort ...", "propagateProxyEnv ...", or an
http_proxy/https_proxy/no_proxymismatch — the same files overriding settings agentctl pins for the instance; remove the override. - "uses vmType ..." — the instance doesn't use Lima's
vzVM type (the macOS default), and agentctl's enforcement is only verified forvz. Recreate it from a template that doesn't force another VM type. - "mounts ... writable, which overlaps agentctl's state directory" —
a writable mount (typically of your whole home directory) would let the
guest edit its own egress policy in
~/.config/agentctl. Make that mount read-only, or share a narrower directory instead. - "beyond what the instance's own configuration mounts" — a mount that
~/.lima/_config/default.yamloroverride.yamladds; see below. - "would forward the host's SSH agent" (or "X11 display") — Lima would forward one of them into the guest; see below.
- "would forward the host socket" or "would copy" — a reverse port
forward or a
copyToHostentry; see below.
Lima: start fails with "beyond what the instance's own configuration mounts"
agentctl: Lima would mount /Users/you at /Users/you (read-only) in instance "demo", beyond what the instance's own configuration mounts, so agentctl won't start it. ...
Lima merges the mounts in ~/.lima/_config/default.yaml and
override.yaml (the error names the exact files) into every instance it
starts, agentctl's included. A mount you set up there for your other Lima VMs
would reach this sandbox too, without passing any of agentctl's mount checks,
so start checks what Lima would actually mount and stops when that's more
than the instance itself declares (see
Lima setup).
Remove the entry from that file. If this sandbox should see the directory, name it to agentctl instead:
$ agentctl start demo --mount ~/src/project:/workspace:w
The check runs on every start, so editing those files can stop a sandbox
that started fine before.
Lima: start fails with "would forward the host's SSH agent" (or "X11 display")
agentctl: Lima would forward the host's SSH agent into instance "demo", so agentctl won't start it: anything in the guest could use it to authenticate as you. ...
Lima forwards your SSH agent into an instance when its ssh.forwardAgent is
on, and your X11 display when its ssh.forwardX11 is. The agent would let
anything in the sandbox authenticate as you, and X11 would give it your
display, so agentctl creates sandboxes with both off, and start stops if Lima
would turn one back on (see
Lima setup).
That happens one of two ways:
~/.lima/_config/override.yamlturns it on (the error names the exact file). Lima appliesoverride.yamlover every instance's own settings, so remove it there. If you want agent forwarding for your other Lima VMs, set it indefault.yamlinstead: an agentctl sandbox turns it off itself, anddefault.yamldoesn't override that.-
The instance doesn't turn it off itself, because an older agentctl created it, or
limactldid, sodefault.yamlor the template still reaches it. Turn it off for this instance, thenstartit again:$ limactl edit --set '.ssh.forwardAgent = false' --start=false demoFor X11, the same with
.ssh.forwardX11.
The check runs on every start, so editing override.yaml can stop a sandbox
that started fine before.
Lima: start fails with "would forward the host socket" or "would copy"
agentctl: Lima would forward the host socket /var/run/docker.sock into instance "demo" (at /run/user/501/docker.sock), so agentctl won't start it: ...
agentctl: Lima would copy /etc/rancher/k3s/k3s.yaml from instance "demo" to /Users/you/.kube/config on the host, so agentctl won't start it: ...
Lima is set to forward a socket on your host into the sandbox (a
portForwards entry with reverse: true), or to copy a file out of the
sandbox onto your host (copyToHost). Either would give the sandbox a way back
to your host, so agentctl won't start it (see
Lima setup).
The entry comes from one of two places:
~/.lima/_config/default.yamloroverride.yaml(the error names the exact files). Lima merges both into every instance it starts, agentctl's included, so a socket or file you set up there for your other Lima VMs reaches the sandbox too. Remove the entry; if another VM needs it, put it in that VM's own configuration (limactl edit <vm>) instead.-
The instance's own configuration, if neither file has it: a custom template with a reverse forward, or, for
copyToHost, an instance an older agentctl orlimactlcreated. Remove it there, thenstartagain:$ limactl edit --set 'del(.portForwards[] | select(.reverse == true))' --start=false demo $ limactl edit --set '.copyToHost = []' --start=false demo
The check runs on every start, so editing those files can stop a sandbox
that started fine before.
Lima: "network confinement check failed"
After every start, agentctl tries a direct connection out from inside the
guest, bypassing the proxy, to confirm it's refused. This error means it
got through — so Lima's processes weren't running under agentctl's macOS
sandbox as expected — and agentctl stopped the instance rather than leave
it running unprotected. Please report it, with your macOS version and
limactl --version.
If instead start prints "network confinement not verified", the check
didn't run: the guest has no curl, no bash with timeout, and no python3
to try the connection with. The instance still started under the
sandbox-exec profile, so its egress is still limited to the proxy — it just
isn't proven from inside this guest. Install curl in the image to get the
check back.
Lima: an instance created by an older agentctl has no network access
Instances created before Lima network enforcement existed have no recorded
egress policy, so their proxy denies everything (failing closed, as on
Incus with an empty allowlist). Recreate the instance with the --allow
flags or profile it should have.
Exit code reference
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Generic runtime error |
2 |
Capability gap (not available / under development) |
3 |
Manual workaround available but not applied/forced |