isx

Give your AI coding agents their own machines — not your credentials.

You're about to hand an AI agent a terminal. On your laptop, that terminal can read your API keys, your GitHub token, your ~/.ssh, and every repo you have checked out — and anything it writes, your IDE and build tools will happily execute.

isx onboards agents the way you'd onboard a new teammate:

Runs on Linux and macOS, on your hardware. Your code and credentials never leave the building.

Agents are the headline, not the limit: the same disposable machines are ideal for triaging untrusted patches, reproducing bug reports, and testing on a clean system — anything you'd rather not run on your host.

Built with Quarkus and Tamboui, powered by Incus system containers. (isx was formerly known as incus-spawn.)

Quick Start

Requires Linux or macOS. On Linux, Incus runs natively and isx init auto-installs it via your package manager. On macOS, isx init provisions a lightweight Linux VM automatically via vfkit. The VM starts automatically when needed and can be managed with isx vm start|stop|status. Windows is not yet supported.

macOS limitations: GUI/audio passthrough (Wayland + PipeWire) and overlay mode for host-resources are Linux-only features. On macOS, use readonly or copy modes for host-resources instead.

On macOS:

brew install Sanne/tap/incus-spawn

On Linux (x86_64):

curl -fsSL https://isx.run | sh

On other Linux architectures:

jbang app install isx@Sanne/incus-spawn
# One-time host setup (Incus, firewall, auth)
isx init

# Build a template (builds parent images automatically)
isx build tpl-java

# Launch the interactive TUI
isx

Fedora users can also install via dnf, and JBang users via jbang — see Installation for all options. Shell completions are available for bash, zsh, and fish via isx completion <shell>.

Credential Isolation

API keys and tokens never enter containers in any form. A host-side MITM TLS proxy (isx proxy) provides completely transparent authentication:

There is no API, endpoint, environment variable, or file that code inside the container can access to obtain real credentials — the injection happens entirely outside the trust boundary.

The proxy must be running for non-airgapped containers. isx init can install it as a systemd user service, or run isx proxy in a separate terminal. The CLI verifies proxy reachability and version compatibility before builds, branches, and shell access.

The proxy also caches container image layers and build artifacts on the host — the same dependency is never downloaded twice (see Caching).

Branching

Like git branch, branching creates an instant copy-on-write clone of any template. Each branch has its own independent filesystem -- changes in one branch cannot affect the template or any other branch. The storage backend (btrfs/zfs/lvm) deduplicates unchanged data automatically, so branches are instant to create and only consume disk space for their own modifications. isx init automatically creates a btrfs storage pool if needed.

tpl-java  (stopped template, ~2GB)
  ├── fix-nasty-bug    (running, uses ~50MB extra)
  ├── review-pr-423    (running, uses ~30MB extra)
  └── experiment       (stopped, uses ~10MB extra)

You can install packages, break things, and destroy a branch when done. The template and other branches are completely unaffected. Sudo works without a password, and shell sessions set the terminal title to isx:<containername> so you always know which environment you're in.

Branches can optionally enable GUI/audio passthrough (Wayland + PipeWire with GPU acceleration, Linux only), restricted networking, or an inbox mount to share files read-only from the host. Resource limits (CPU, memory, disk) are auto-detected from the host but can be overridden. The interactive TUI (isx with no arguments) provides a Midnight Commander-style interface with modal dialogs for branching, renaming, and building, plus F3 detail views and F9 tool actions.

Templates pre-install your baseline tools and repos, and integrations plug in through the same tool system: VS Code Remote, JetBrains Gateway, shell completions, and Claude Code skills.

Network Modes

Each branch runs in one of three network modes:

Mode Flag Description
Full internet (default) Unrestricted network access via NAT, auth via MITM proxy
Proxy only --proxy-only Outbound traffic restricted to MITM proxy only (iptables)
Airgapped --airgap Network device removed, complete isolation

Git Remotes

Containers created with isx branch are isolated environments, but you need a way to get your changes back. isx integrates with git's native remote helper protocol so you can use standard git fetch, git push, and git pull between host repos and container repos:

# Inside the container, the agent makes some commits...
# Back on the host:
git fetch fix-auth
git diff main..fix-auth/main    # review exactly what it did
git cherry-pick fix-auth/main   # take what you like

This is the intended review workflow: you always act on a specific, immutable commit — never on a live directory the agent can still modify (see the FAQ for why there is deliberately no read-write project mount).

isx:// URLs

The remote uses the isx:// URL scheme (~ expands to /home/agentuser):

git remote add fix-auth isx://fix-auth/~/quarkus
git fetch fix-auth
git diff main..fix-auth/main

The instance must be running for git operations to work.

Automatic remotes

If you configure host-paths in ~/.config/incus-spawn/config.yaml, remotes are managed automatically:

# Base directories where your repos live on the host
# If a repo exists in multiple host-paths, you must add an explicit repo-paths entry
host-paths:
  - ~/projects
  - ~/workspace

# Explicit overrides for repos in non-standard locations or to resolve ambiguity
repo-paths:
  quarkus: ~/work/quarkus
  hibernate: /opt/hibernate

With this configuration, isx branch adds a git remote named after the instance in each matching host repo (protocol-lenient — SSH and HTTPS URLs for the same repo are treated as equal), and isx destroy removes it.

Why full system containers?

Docker and Podman are built for shipping applications — minimal filesystems, single-process isolation, fast startup. isx solves a different problem: full system containers powered by Incus that behave like real machines. Each environment runs its own init system, has real networking (ping, strace, nested Podman/Docker), and supports GUI and audio passthrough (Linux only). Templates pre-install your baseline tools and repos, but the environment is a real Linux system — agents and users can freely dnf install, pip install, build from source, or run Docker Compose just like on a workstation.

This matters for agents in particular: an agent boxed into an app container hits walls constantly (no systemd services, no nested containers for Testcontainers, no debugging tools). An agent on an isx branch works exactly as it would on a developer workstation — because that's what it has.

For untrusted code, KVM virtual machines (--vm) provide hardware-level isolation with a separate kernel.

Template Images

Template images are reusable base environments defined in YAML. They can inherit from each other -- building an image automatically builds any missing parents:

# images/java.yaml
name: tpl-java
description: JDK + Maven + Claude Code
parent: tpl-dev
packages:
  - java-25-openjdk-devel
  - java-25-openjdk-javadoc
  - java-25-openjdk-src
tools:
  - maven-3

Three images are built-in (tpl-minimal, tpl-dev, tpl-java). The root image (tpl-minimal) uses a custom Fedora base from Sanne/incus-spawn-images. Use isx update-base to check for new base image releases, pin a specific version, or track the latest:

isx update-base              # interactive — shows versions, prompts for action
isx update-base --list       # list available versions
isx update-base --latest     # always track the newest version
isx update-base fedora-44-v2 # pin to a specific release tag

Pinning writes a user-level override to ~/.config/incus-spawn/images/minimal.yaml. Tracking latest (the default) uses the built-in definition, which is updated with each isx release. After changing the base image version, rebuild with isx build tpl-minimal.

Add your own templates by placing YAML files in ~/.config/incus-spawn/images/ (user-level) or .incus-spawn/images/ (project-local). You can also point to external directories via searchPaths in config.yaml (see Configuration).

Use isx templates to manage templates from the CLI:

# List all available templates
isx templates list
isx templates list -v          # with source path and description

# Create a new template (opens in $EDITOR with a commented skeleton)
isx templates new my-app       # creates ~/.config/incus-spawn/images/my-app.yaml
isx templates new my-app --project  # creates .incus-spawn/images/my-app.yaml

# Edit an existing template
isx templates edit tpl-java    # opens in $EDITOR, validates on save

Editing a built-in template automatically creates a user-level override in ~/.config/incus-spawn/images/. The override takes precedence over the built-in but will not auto-update with isx upgrades. Templates are validated after editing: YAML syntax, required fields, and parent references are checked.

You can also define a custom root image (no parent) by specifying image, image_url, image_tag, and image_sha256 to point at your own pre-baked OS tarball. See the built-in minimal.yaml and the incus-spawn-images repo for the reference example.

Image schema fields (all optional except name):

# Build a specific image (builds missing parents automatically)
isx build tpl-java

# Rebuild a template and all its parents from scratch
isx build tpl-java --with-parents

# Rebuild out-of-sync templates (changed definitions or older isx version)
isx build --out-of-sync

# Rebuild all discovered images from scratch
isx build --all

The TUI marks templates with ! when they were built with a different isx version, and when the image or tool definition has changed since the last build — isx build --out-of-sync rebuilds these automatically. If a build fails, the container is promoted to an inspectable instance so you can shell in and debug.

Declarative Repos

Images can declare git repositories to clone into the container. Declaring a git repository rather than using shell commands to fetch it allows for better integration into other tools, such as Claude Code.

name: tpl-quarkus
description: Quarkus development
parent: tpl-java
tools:
  - podman
  - gradle
repos:
  - url: https://github.com/quarkusio/quarkus.git
    path: ~/quarkus
    prime: mvn -B dependency:go-offline

Repo entry fields:

Declared repos are automatically pre-trusted in .claude.json so Claude Code doesn't prompt for trust on first use.

Shell Defaults

Templates can configure the default working directory, shell command, and default action when connecting to a container:

name: tpl-quarkus
parent: tpl-java
tools: [claude]
repos:
  - url: https://github.com/quarkusio/quarkus.git
    path: ~/quarkus
workdir: ~/quarkus
default-action: claude

Claude Code

Claude Code is Anthropic's official CLI for Claude. Add it to any template with tools: [claude]:

name: tpl-agent
description: Isolated dev environment with Claude Code
parent: tpl-dev
repos:
  - url: https://github.com/myorg/myproject.git
    path: ~/myproject
workdir: ~/myproject
tools:
  - claude
default-action: claude

The claude tool downloads the latest Claude Code binary, configures permissions for unattended agent use, and sets up authentication. All three auth modes work transparently — the MITM proxy injects credentials so no real API keys or tokens enter the container.

To preconfigure which model Claude Code uses, pass the model parameter:

tools:
  - claude:
      model: claude-sonnet-4-6

When omitted, Claude Code uses its own default. Model IDs follow the claude-* naming convention (e.g. claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5-20251001).

Pi Coding Agent

Pi is a provider-agnostic CLI coding agent that uses the standard Anthropic API. Add it to any template with tools: [pi]:

name: tpl-pi-dev
description: Isolated dev environment with Pi coding agent
parent: tpl-dev
repos:
  - url: https://github.com/myorg/myproject.git
    path: ~/myproject
workdir: ~/myproject
tools:
  - pi
shell-command: pi

Pi works out of the box with all three auth modes (API key, Claude Pro/Max OAuth, Vertex AI) — the MITM proxy injects credentials transparently. To use Pi without making it the default shell, omit shell-command and launch it manually after isx shell.

Claude Code Skills

Template images can declare Claude Code skills to bake in at build time. Skills are installed once into the template and inherited by every instance branched from it.

name: tpl-agent
description: Agent with security skills
parent: tpl-dev
skills:
  repo: myorg/claude-skills      # default catalog for bare skill names
  list:
    - security-review            # short name → myorg/claude-skills@security-review
    - code-review                # short name → myorg/claude-skills@code-review
    - xixu-me/skills@xget        # explicit owner/repo@skill-name
    - myorg/catalog              # all skills from a repo

There is no implicit default catalog -- repo is only needed to resolve bare skill names (like security-review above). When all entries use the fully qualified owner/repo@skill or owner/repo form, you can omit repo and use the list shorthand:

skills:
  - xixu-me/skills@xget
  - myorg/catalog

Skill source formats:

To find available skills, browse skills.sh.

Host Resources

Template images can declare host files and directories to make available inside containers. This is useful for sharing configuration files, pre-populating caches, or providing large datasets without copying them into every template.

name: tpl-java
parent: tpl-dev
packages:
  - java-25-openjdk-devel
tools:
  - maven-3
host-resources:
  - source: ~/.m2/repository
    mode: overlay
  - source: ~/.gitconfig

The ~/.m2/repository entry shares your host Maven cache with the container. With mode: overlay, the container sees a normal read-write directory pre-populated with your cached artifacts, but writes go to a container-local layer -- your host cache is never modified. Maven builds that would normally download hundreds of megabytes of dependencies can instead resolve them instantly from the shared cache.

The ~/.gitconfig entry mounts your git configuration read-only (the default mode), so git inside the container picks up your name, email, aliases, and other settings.

Three modes are available:

Mode Default? Description
readonly Yes Read-only bind mount. Simple, safe.
overlay No Read-only lower layer from host + ephemeral writable upper in the container. Tools see a normal read-write directory. Host is fully protected. Linux only — not yet supported on macOS.
copy No Copied into the container at build time. Becomes part of the template. Also supports URL sources.

If path is omitted, it defaults to the same relative path under /home/agentuser/. Missing host paths are skipped with a warning, so templates remain portable. Host resources compose across the parent chain, with child entries overriding parent entries matched by container path.

Custom Tools

Template inheritance forms a single chain -- a template has exactly one parent. Tools provide composition: reusable capabilities that any template can mix in independently. A gradle tool can be added to a Java template, a Kotlin template, or a project-local template without duplicating definitions or creating diamond inheritance.

Tools are defined as YAML files and referenced from image definitions via tools::

# .incus-spawn/tools/gradle.yaml
name: gradle
description: Gradle 9.4.1

downloads:
  - url: https://services.gradle.org/distributions/gradle-9.4.1-bin.zip
    sha256: 2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
    extract: /opt
    links:
      /opt/gradle-9.4.1/bin/gradle: /usr/local/bin/gradle

verify: gradle --version

Downloads declared this way are cached on the host at ~/.cache/incus-spawn/downloads/, so rebuilding images doesn't re-download unchanged artifacts. Extraction happens on the host -- the container doesn't need tar, unzip, or curl.

Tool schema fields (all optional except name):

Download entry fields:

Supported archive formats: .tar.gz/.tgz, .tar.bz2, .tar.xz, .zip.

Execution order during install(): packages → downloads → runrun_as_userfilesenvverify. Resolution follows the same order as templates (see Configuration).

Remote IDE Access

Both VS Code and JetBrains IntelliJ can connect to containers with their UI running natively on the host and all backend processing (indexing, builds, terminals, extensions) running inside the container. SSH keys are managed automatically: isx init generates a dedicated passphraseless key pair at ~/.config/incus-spawn/ssh/, and each branch injects it into the container along with your personal ~/.ssh key. Container host keys are pre-validated so ssh <instance-name> just works — no passphrase prompt, no host key warning. Entries are cleaned up when instances are destroyed.

Both tools declare TUI actions — press F9 on a running instance to open a repo directly in your IDE.

VS Code (Remote - SSH)

The built-in vscode-remote tool provides one-click "Open in VS Code" actions. It declares requires: [sshd], so the SSH server is installed automatically. No backend is pre-installed inside the container — VS Code downloads its own server component on first connect.

Host prerequisite: install the Remote - SSH extension in VS Code.

name: tpl-java-vscode
parent: tpl-java
tools:
  - vscode-remote    # auto-installs sshd via requires

JetBrains IntelliJ (Gateway)

The built-in idea-backend tool installs the JetBrains IntelliJ IDEA remote development backend inside the container. It declares requires: [sshd], so the SSH server is installed automatically.

Host prerequisite: install JetBrains Gateway.

name: tpl-java-ide
parent: tpl-java
tools:
  - idea-backend    # auto-installs sshd via requires

The idea-backend tool accepts a memory parameter to control the JVM heap size (default 2g). Use the map form to customize it:

tools:
  - idea-backend:
      memory: "8g"

Terminal Session Persistence (zmx)

zmx provides session attach/detach for the terminal — persistent shell sessions that survive disconnections, with native scrollback and multi-client support. Unlike tmux, it delegates window management to your OS rather than reimplementing it.

name: tpl-agent
parent: tpl-dev
tools:
  - zmx

By default, auto_attach is enabled: shelling into the container automatically attaches to a persistent zmx session named isx. To install zmx without auto-attaching:

tools:
  - zmx:
      auto_attach: "false"

On Linux, the container's zmx socket directory is shared with the host via a disk device. Container sessions appear in your native zmx socket directory with an isx- prefix — no configuration needed:

zmx list                              # shows isx-my-branch alongside local sessions
zmx attach isx-my-branch              # attach to the container's session from the host
zmx history isx-my-branch             # view scrollback from the container
zmx run isx-my-branch git status      # run a command in the container's session

All zmx commands work natively, including interactive zmx attach. On macOS, where Incus runs inside a VM, host-side socket sharing is not available. zmx sessions still work inside containers — isx shell (or Enter in the TUI) auto-attaches, and sessions persist across disconnections.

Tool Parameters

Tools can define parameters for build-time configuration. Parameter types: string (with optional pattern), integer (with min/max), boolean, and enum (with options). Use ${param_name} to reference values in scripts, env, and file content:

# tools/my-server.yaml
name: my-server
parameters:
  memory:
    type: string
    default: "2g"
    pattern: "^[0-9]+[gGmM]quot;
env:
  - export SERVER_MEMORY=${param_memory}

Pass parameter values using the map form in image definitions (the idea-backend memory example above shows this pattern).

Tool Actions

Tools can declare runtime actions that appear in the TUI (press F9 on a running instance, or Enter to run the template's default action). Actions can be declared in YAML tool definitions or programmatically by Java/CDI tools. The built-in claude and pi tools automatically contribute shell actions ("Claude Code" and "Pi Coding Agent") when included in a template's tools list.

Action entry fields:

Type-specific fields:

Template variables available in label, url, command, and text: ${ip}, ${name}, ${parent}. When expand: repos is set, repo-specific variables are also available: ${repo_name}, ${repo_path}, ${repo_url}.

actions:
  - label: "Open repo '${repo_name}' in Gateway"
    type: url
    expand: repos
    url: "jetbrains-gateway://connect#host=${ip}&projectPath=${repo_path}"
  - label: "Launch agent"
    id: launch
    type: shell
    command: "my-agent --continue"
    auto_return: true

Caching

The proxy and build system cache artifacts on the host, shared across all templates and branches. Only immutable, content-addressed artifacts are cached — mutable data (Maven SNAPSHOTs, repository metadata, version listings) always passes through uncached. Every artifact is verified against its content digest or upstream checksum before being committed to the cache; mismatches are discarded and re-fetched.

Proxy caches (from container traffic):

Build-time caches:

All caches live under ~/.cache/incus-spawn/. There is no automatic eviction — every entry is content-addressed or version-pinned, so it is either correct forever or superseded by a newer version with its own entry.

Roadmap

isx is evolving from a container manager into mission control for parallel coding agents: per-agent identities and audited commit signing (#271), proxy-derived monitoring of agent status and spend, task dispatch, and an in-TUI review lane (#322). Local-first stays the core conviction — your hardware, your network, your repos. See docs/VISION.md for the full direction.

Installation

macOS (Homebrew)

brew install Sanne/tap/incus-spawn

Updates with brew upgrade incus-spawn. See docs/HOMEBREW.md for details.

Fedora (DNF)

sudo dnf copr enable sanne/incus-spawn
sudo rpm --import https://download.copr.fedorainfracloud.org/results/sanne/incus-spawn/pubkey.gpg
sudo dnf install incus-spawn

Updates automatically with sudo dnf upgrade.

Any Linux distro (native binary)

curl -fsSL https://isx.run | sh

Installs a self-contained native binary to ~/.local/bin/isx. No JVM required. Set INSTALL_DIR to change the install location. To update, re-run the same command. To uninstall, run uninstall.sh (caches at ~/.cache/incus-spawn/ are preserved unless you pass --purge).

JVM via JBang

jbang app install isx@Sanne/incus-spawn

Configuration

The config.yaml also supports git remote auto-management via host-paths and repo-paths (see Git Remotes), and a searchPaths list for loading templates and tools from external directories. Each directory should contain images/ and/or tools/ subdirectories following the same YAML schema as the built-in definitions. Tilde (~) expansion is supported for all path settings:

searchPaths:
  - ~/my-templates
  - /absolute/path/to/templates
my-templates/
  images/
    quarkus.yaml
  tools/
    gradle.yaml

Resolution order (later sources override earlier ones with the same name):

  1. Built-in (bundled with isx)
  2. User (~/.config/incus-spawn/)
  3. Search paths (in listed order)
  4. Project-local (.incus-spawn/)

FAQ

Why can't I mount a host directory read-write to follow agent work in my IDE?

A project directory is not just data — it is an implicit code execution channel. Build tools, package managers, and IDEs all trust its contents and execute them with your full host privileges. A read-write mount turns the agent's output into unreviewed host-side code execution, which is exactly the threat model isx exists to prevent.

Two attack surfaces make this dangerous even for "just the project directory":

  1. Executable project content. Build plugins, Makefiles, gradlew, .mvn/jvm.config, git hooks, IDE run configurations, and dependency declarations with local path references (<systemPath>, file: deps, Go replace directives) all execute when you run a normal build command. An agent that modifies a Maven build plugin or a git pre-commit hook gets code execution on your host with your credentials and network access — without you ever intentionally "running the agent's code."

  2. IDE auto-execution. VS Code, IntelliJ, and most editors auto-execute project configuration the moment you open a directory: .vscode/settings.json (task auto-run), .idea/ workspace files, ESLint/TypeScript/Pyright configs that load plugins. An agent writing to the project directory can trigger code execution on your host just by the folder being open — no build command required.

These risks are compounded by a race condition: with a live read-write mount, files can change between review and execution. You inspect a git hook or build script, decide it's safe, and run your build — but the agent modified the file between your review and your command. Unlike git fetch, which gives you a specific immutable commit to review and act on, a live mount means your review is never final.

Beyond security, a shared project directory is also misleading. The agent's code runs against the container's execution context — container-local SNAPSHOT dependencies, container-local node_modules, container-local pip packages. None of that comes through the mount. The source code on the host looks like a complete project, but when you build it locally it may behave differently or break entirely because the dependency state is invisible. The project directory is only a partial view of the agent's environment.

What to use instead:

CLI Reference

Command Description
isx Launch the interactive TUI
isx init One-time host setup (Incus, firewall, auth)
isx build <template> Build or rebuild a template (--all, --missing, --out-of-sync, --with-parents)
isx branch <name> Create a CoW clone from a template or instance
isx shell <instance> Open a shell in an instance
isx destroy <instance> Destroy an instance
isx update-base Check for and install base image updates (--list, --latest, or a tag)
isx update-all Update all templates (packages, repos, tools)
isx templates List available templates
isx templates list -v List templates with source and description
isx templates new <name> Create a new template definition
isx templates edit <name> Edit a template in $EDITOR
isx instances List connectable instance names (excludes templates)
isx project create <name> Create a project template from incus-spawn.yaml
isx project update <name> Update an existing project template
isx proxy start Start the MITM auth proxy
isx proxy stop Stop the proxy
isx proxy status Show proxy status
isx proxy install Install proxy as a systemd user service
isx proxy uninstall Stop and remove the systemd proxy service
isx proxy logs View proxy logs
isx proxy dump Run a local pass-through proxy for API traffic capture
isx doctor Diagnose host, proxy, VM, and tunnel health
isx vm start Start the VM (macOS only)
isx vm stop Stop the VM (macOS only)
isx vm status Show VM status and system diagnostics (macOS only)
isx vm console Follow VM serial console output (macOS only)
isx completion <shell> Print shell completion script (bash, zsh, fish)

Use isx <command> --help for detailed options on any command.