Configuration
isx init writes your settings and credentials.
This page says where they live, how a credential gets several accounts that templates and instances choose between, which host services instances can reach, and what isx caches on the host.
| File | What it holds |
|---|---|
~/.config/incus-spawn/config.yaml |
Auth credentials and global settings, including host-routes:, exposed-host-ports: and drivers: |
~/.config/incus-spawn/mcp.yaml |
What isx mcp lets agents do (see Delegating from an agent) |
~/.config/incus-spawn/ssh/ |
The managed SSH key pair and the per-instance SSH config |
~/.config/incus-spawn/images/*.yaml |
User-level template definitions |
~/.config/incus-spawn/tools/*.yaml |
User-level tool definitions |
.incus-spawn/images/*.yaml |
Project-local template definitions, the ones a cloned repository ships in its .incus-spawn/ directory |
.incus-spawn/tools/*.yaml |
Project-local tool definitions |
Credential accounts
You can configure a credential more than once under different names, for example a personal and a client Claude subscription, or two GitHub identities. Each named copy is an account, and you choose which account each instance uses. You can even switch a running instance to another account, with no restart.
claude:
accounts:
personal:
type: oauth
oauthToken: "sk-ant-oat01-..."
acme:
type: vertex
cloudMlRegion: europe-west1
vertexProjectId: acme-prod
default: personal
github:
accounts:
personal:
token: "ghp_..."
acme-bot:
token: "ghp_..."
email: "bot@acme.example"
default: personal
You do not need to write this by hand: isx init adds, renames and removes accounts and sets the default.
Re-run it any time to change them.
isx ignores a key it does not read in a tool's credential section, such as openai.token written where openai.apiKey is meant.
isx init, isx account add and isx doctor name the ignored key, and so does the error when a template needs that credential.
A subscription account serves Claude Code in your instances, but not isx ask, which needs an API key.
Claude Max and Team plans include monthly API credits for an API key, claimed on claude.ai under Settings > Billing.
Add the key as a second claude account with type: api-key, and keep the subscription as the default.
isx init offers this when it saves a subscription.
An instance uses the account from the first of these that is set:
- its own choice:
isx branch --account,isx account set, or theakey in the TUI; - its template's
accounts:; - the credential's
default.
name: tpl-acme
parent: tpl-java
accounts:
claude: acme
github: acme-bot
When you branch from a template, the new instance takes the template's account choices and keeps them.
Editing the template's accounts: later affects only instances branched after the edit.
Example: keeping a client's work on the client's accounts
isx build tpl-acme
isx branch acme-42 --from tpl-acme
isx account show acme-42
acme-42 (branched from tpl-acme):
claude acme -- Google Cloud Vertex AI (region: europe-west1, project: acme-prod)
Pinned by template tpl-acme's accounts: setting, copied onto this instance when it was branched.
github acme-bot -- commits as bot@acme.example
Pinned by template tpl-acme's accounts: setting, copied onto this instance when it was branched.
Everything in acme-42 uses Acme's accounts.
For a single instance, choose the account when you branch: isx branch review-1 --account github=acme-bot.
Managing an instance's accounts
isx account list # accounts, and which instances use each
isx account add github # add or replace a GitHub token or account
isx account show review-1 # what review-1 uses, and why
isx account set review-1 github=acme-bot
isx account unset review-1 github # follow the default again
In the TUI, F3 on an instance shows its accounts and the a key changes them.
The branch dialog (F4) lets you pick accounts too.
Changes take effect on the instance's next request, with no restart. An instance switched to another GitHub account takes that account's git identity on its next start or shell. The exception is switching between Claude auth modes (Pro/Max, API key, Vertex), which needs a template built for that mode (#866).
Changing a default switches every instance that follows it to the new account; isx init lists those instances first and lets you keep them on their current account.
Renaming an account in isx init updates the instances using that account, and when you remove an account, isx init tells you what still uses it.
An instance set to an account that no longer exists stops working rather than using another account.
isx doctor and isx account show report such instances.
Inheriting the outer instance's account
An isx running inside an isx instance serves its own instances from its own config.yaml.
The reserved account inherit lets those inner instances use, for one credential, the account of the outer instance, which is the instance that isx runs in.
Select inherit like any other account, in any of these ways:
isx branch inner-1 --account github=inherit;isx account set inner-1 github=inherit;- a template's
accounts:; default: inheritundergithub:in the innerconfig.yaml.
isx gives each instance a proof, a token that the instance sends in place of a credential.
With inherit, the inner proxy presents the outer instance's proof in place of the inner instance's own proof, so the outer proxy serves the outer instance's account for that credential only.
The inner proxy checks its instance's current proof before substituting the outer instance's proof. CA trust and the outer network policy still apply.
inherit does not share anything else.
A nested process with no proof, a forged proof-shaped token, or an independent account gets no inherited credential.
Outside an isx instance, inherit is refused when you branch, and so is any request that relies on it.
You cannot name an account of your own inherit.
isx doctor counts an inherited default as configured inside an instance whose secret isx doctor can read, and warns about the default otherwise.
A template whose own accounts: says claude: inherit builds Claude for an API key, the form of request that the outer proxy converts to whichever account it serves.
The git identity of an instance that inherits GitHub is the outer account's at the time the instance starts inheriting.
If the outer instance later switches GitHub accounts, branch again or run isx account set to take the new account's identity.
Git cache
Builds can read Git objects directly from linked host checkouts without changing the checkouts or copying their object stores first.
A full clone's snapshot, a clean copy of the repository that the build prepares on the host, holds the branch or tag the template asks for, or else the remote's default branch, with all its history. The instance fetches other branches itself when it needs them; shallow clones retain their configured depth.
Each build's copy also gets the tags in that history, so git describe gives the same answer as on a regular clone.
For full clones, a bounded cache of clean snapshots avoids repeated downloads and packing, including when a linked checkout exists. Normal upstream updates add new objects to the verified cache without repacking its history. The cache has two limits, shown here with their defaults:
git-cache:
snapshot-max-mib: 16384
max-age-days: 7
Set either limit to 0 to disable persistent retention.
A snapshot larger than snapshot-max-mib is used for its build but not kept, so every build clones and verifies that repository in full again.
The build warns:
- when a snapshot is too large to keep, with the snapshot's size and the setting to raise;
- on every build while retention is disabled;
- when keeping a snapshot evicts other recently used ones to stay within
snapshot-max-mib, since repositories built in turn would then evict each other and be cloned in full on every build.
Active builds keep their temporary data until their reference mounts are detached. See Git preparation for details.
Git transfers on the host
Git transfers on the host are isx update-all fetching linked checkouts, and a build preparing a clean snapshot.
They show their elapsed time and Git's progress as they run.
Neither Git nor ssh stops to ask a question: a transfer that would ask fails at once instead.
What you see when a transfer fails:
- What isx changed and what to set up.
- Git's own output under
Git's output:, each line marked with|. - When SSH fails, what isx did.
What to do before a transfer:
- Load the key into
ssh-agent: ssh does not ask for a passphrase. - Keep the host in
~/.ssh/known_hosts: ssh does not ask about a host key. - Do not rely on a password prompt: Git fails at once instead of asking for one.
Where it can, isx adds -o BatchMode=yes to ssh or to your core.sshCommand, which stops both ssh prompts; for a build, only a global core.sshCommand applies.
isx makes any other ssh, such as a custom GIT_SSH or a wrapper, fail rather than ask (OpenSSH 8.4 or later).
isx ends a transfer that stops making progress, instead of waiting for the transfer to finish:
git-transfer:
stall: 5m # ended after this long with no output, no I/O and next to no CPU in Git or its helpers
timeout: 3h # ended after this long in any case
These are the defaults; 0 turns a bound off.
Quiet work such as resolving deltas still uses the CPU, so it is not a stall.
An ended isx update-all fetch is a warning, and the checkout stays unchanged.
An ended build preparation falls back to cloning inside the build.
Search paths for templates and tools
config.yaml also holds host-paths and repo-paths, with which isx manages git remotes for you (see Git Remotes), and a searchPaths list for loading templates and tools from external directories.
Each directory holds images/ and/or tools/ subdirectories following the same YAML schema as the built-in definitions.
Every path setting expands a tilde (~):
searchPaths:
- ~/my-templates
- /absolute/path/to/templates
my-templates/
images/
quarkus.yaml
tools/
gradle.yaml
isx loads definitions in this order, and a later source overrides an earlier definition with the same name:
- Built-in (bundled with isx)
- User (
~/.config/incus-spawn/) - Search paths (in listed order)
- Project-local (
.incus-spawn/)
External tools that drive isx
drivers: says what an external tool, a program outside isx that creates instances for you, such as an orchestrator, may do to the instances it owns.
An external tool owns an instance it branched with isx branch --owner <name>.
isx does not grant an external tool any permission unless you list the permission here:
drivers:
orca:
destroy: true # isx destroy --owner orca may delete orca's instances
| Key | Default | What it does |
|---|---|---|
destroy |
false |
Lets isx destroy --owner <name> delete the instances that tool owns |
An external tool never destroys an instance it does not own, such as your own branch of one of its instances. An entry isx cannot read does not grant any permission, and isx says which entry and why.
A service on the host
A service listening only on your host's loopback, such as Ollama, can be made reachable from every instance.
List its port in ~/.config/incus-spawn/config.yaml:
exposed-host-ports: [11434]
Instances then reach it at host.isx.internal:11434, for example with this in the template:
env:
- name: OLLAMA_HOST
value: host.isx.internal:11434
The proxy listens on that port at the bridge address, the host address where instances already reach the proxy.
It passes each connection unchanged to the same port on 127.0.0.1, or on [::1] when nothing answers on 127.0.0.1.
Nothing is installed in the instance, and the service keeps listening on loopback alone.
| What | How it behaves |
|---|---|
| Who can connect | Every instance with network, including one built from a cloned repository's own template |
| Other machines on your network | Refused, even when your firewall lets them reach the bridge address |
--proxy-only and --airgap |
Cannot reach any exposed port |
| What passes | Raw TCP: no TLS, no headers added, no credentials, no path filter |
| What the service sees | A caller on loopback |
| Unlisted loopback ports | Stay unreachable |
| A port isx uses (443, 18443, 18080, 53) | Refused, with a warning in the proxy's log |
What exposing a port gives away
Expose a port only when you would let any instance use that service as you do. Many development services trust any caller on loopback completely. isx does not filter the connections it passes to the service, so an instance can do whatever the service allows.
There is no list of templates and no caller identity, so every instance with network gets the same access.
Ollama is an example of the access an exposed port gives. Its API has no authentication, and it can pull, create, push and delete models. Exposing 11434 gives every instance all of those operations, and the use of any cloud account Ollama is signed in to.
A service that already listens on every address, or on the bridge address, was reachable from instances before you listed its port, where your firewall allows it. Listing its port does not change how instances reach it.
On Linux the proxy cannot bind such a port on its own address, and isx doctor warns that instances reach whatever program holds the port.
On macOS the proxy binds the port all the same, so instances reach the service through the proxy and there is nothing to warn about.
Host CA propagation
By default an instance trusts only the isx CA and Fedora's roots. On a host behind TLS inspection, or for an isx inside another isx, you can make isx install the host's own CA anchors in every instance too:
host-ca:
propagate: true
See Host CAs for what isx installs and where.
TUI live refresh
The TUI updates live as instances are started, stopped, created or deleted elsewhere. To turn this off:
tui-live-refresh: false
Proxy ready timeout
isx proxy install and isx proxy start wait for the proxy to become healthy after starting the service.
The default is 30 seconds; a healthy proxy answers at once, so a generous default does not slow a healthy start.
To change it:
ready-timeouts:
proxy: 1m # durations: 30s, 1m, 2m ...
The same section holds container and vm: how long a start waits for the instance to answer before the start fails.
Their defaults are 30s and 2m.
Storage pool
Every instance lives on one btrfs copy-on-write pool, which isx init creates when Incus has none.
On Linux, how isx backs the pool depends on the filesystem under /var/lib/incus.
| Host filesystem | Backing | Size |
|---|---|---|
| btrfs | a subvolume of that filesystem | shares the filesystem's free space |
| anything else | a sparse loop file, /var/lib/incus/disks/cow.img |
100 GiB, thin-provisioned |
A loop file on btrfs would nest one btrfs inside another. Every write in an instance would then be copied twice, with two sets of metadata.
isx mounts the loop file to commit every 300 seconds, not 30, since instances hold disposable working data. isx also compresses data inside the pool, since no layer under the pool compresses it.
On macOS the pool lives on the appliance's own disk, the appliance being the Linux VM that isx runs; isx vm resize grows it.
A subvolume pool has no cap of its own, so it can fill the host's disk.
isx doctor warns when the pool is at least 90% full and has less than 5 GiB free.
Each instance's --disk limit still applies.
A subvolume pool also changes the host filesystem as a whole.
Incus enables btrfs quotas to enforce the --disk limit, and they cover the whole filesystem.
Each time Incus starts on the host, it mounts the filesystem with user_subvol_rm_allowed.
That option lets a user delete any subvolume they can write to, not only in the pool.
isx init sets up a new pool only.
For a pool that is already a loop file on btrfs, isx doctor warns and links to the steps for moving it.
Caching
The proxy and build system cache downloads on the host, shared by all templates and branches. isx caches only content it can verify: isx checks everything against its digest or upstream checksum before storing it.
Proxy caches, from instance traffic:
- Container image layers: OCI blobs from Docker Hub, GHCR and Quay, keyed by SHA256 content digest.
- Maven and Gradle artifacts: release JARs, POMs and plugins from Maven Central and the Gradle plugin portal, verified against the upstream checksum and re-confirmed with upstream as they age (a
HEADon Maven Central, the checksum file elsewhere). When the repository is unreachable, the proxy serves cached artifacts unconfirmed, so builds keep working offline. - Gradle distributions: verified against the
.sha256file upstream publishes beside the distribution, and re-confirmed against it the same way. - npm tarballs: from
registry.npmjs.org, verified against the version's published SHA-512 integrity, or its SHA-1 shasum for old versions without one. The proxy re-confirms the tarballs against that value the same way, so the proxy stops serving a version removed upstream.
How recently a cached Maven or Gradle artifact, or npm tarball, must have been confirmed with upstream is configurable:
- Within
freshof its last confirmation, the proxy serves the artifact from the cache without asking upstream. - Within
max-stale, the proxy serves the artifact at once and confirms it again in the background; the proxy evicts a copy that upstream has changed or withdrawn, so the next request does not receive that copy. - Older than
max-stale, the proxy confirms the artifact before serving it.
Gradle Plugin Portal artifacts are always confirmed first, since the portal can delete a published version. The defaults:
artifact-cache:
fresh: 2h # durations: 30m, 2h, 7d ...
max-stale: 7d
Setting both to 0 makes the proxy confirm every cached artifact before serving it.
This is not recommended: Maven Central does not allow published artifacts to change, so confirming every time only adds latency.
Build-time caches:
- DNF packages, shared by every build, so builds reuse each other's downloads.
- Tool downloads, reused by rebuilds.
The DNF cache is a volume in the Incus storage pool; every other cache lives under ~/.cache/incus-spawn/.
isx clean cache removes them all.
Nothing expires by age or size; an artifact is dropped only when upstream changes or withdraws it.