code-on-incus

by mensfeldVerified

Give each AI agent its own isolated machine with root, Docker, and systemd. Active defense detects and stops threats automatically.

661
Stars
55
Forks
Go
Language
8/23/2026
Added
View on GitHubDownload ZIP

⚠️ Third-Party Software Notice

This skill is third-party open-source software developed and hosted independently on GitHub. SkillTip is an informational directory and does not control or maintain the underlying repository. Any security checks displayed are automated and limited in scope. Review the source code before installing.

Read the Terms of Service

Installation

Add to your Claude Code skills directory:

# Add to your Claude Code skills
git clone https://github.com/mensfeld/code-on-incus

Getting Started

Guides for using skills like code-on-incus.

Security Report

Verified

Last scanned: —

{
  "status": "PASSED",
  "issues": []
}

README.md

Code on Incus Logo

code-on-incus (coi)

License: MIT Go Version Latest Release Join the chat at https://slack.karafka.io

Isolated machines for AI coding agents - with active defense.

COI gives each AI agent its own machine - a full system container with root access, systemd, Docker, and the ability to install anything. Agents work like they would on a real server: run services, manage packages, use cron - without touching your actual system. Files stay correctly owned, no permission hacks needed.

Your credentials stay on the host. SSH keys, environment variables, and Git tokens are never exposed to AI tools unless you explicitly mount them. If something goes wrong, COI catches it - reverse shells, credential scanning, data exfiltration - and pauses or kills the container automatically. No manual intervention needed.

Built by developers, for developers who run AI agents and want to know what those agents are doing. Not a product, not a startup - a tool that does the job.

Who this is for

  • You run AI coding agents and want them to have full machine access - root, Docker, package managers, services - without risking your host
  • You want to know when an agent does something suspicious, not find out after the fact
  • You run multiple agents in parallel and need them isolated from each other
  • You want persistent dev environments that survive restarts and reboots, not throwaway containers that lose your setup every time
  • You care about your credentials not ending up inside an agent-controlled environment

BetterStack video about Code on Incus
Watch the BetterStack video about Code on Incus

Demo

Table of Contents

Supported AI Coding Tools

Currently supported:

Coming soon:

  • Aider - AI pair programming in your terminal
  • Cursor - AI-first code editor
  • And more...

Tool selection is config/profile-driven:

# ~/.coi/config.toml or ./.coi/config.toml
[tool]
name = "opencode"            # or "claude" (default), "pi", "codex"
coi shell                    # Uses the configured tool (Claude Code by default)
coi shell --profile opencode # Or switch via a profile with [tool] name = "opencode"

Permission mode - Control whether AI tools run autonomously or ask before each action:

# ~/.coi/config.toml or .coi/config.toml
[tool]
name = "claude"              # Default AI tool
permission_mode = "bypass"   # "bypass" (default) or "interactive"

For Claude, bypass maps to --permission-mode bypassPermissions; for codex it maps to --dangerously-bypass-approvals-and-sandbox (the container is the sandbox), and interactive keeps codex's own approval prompts (-s workspace-write -a on-request).

Codex authentication: coi seeds the host's ~/.codex/auth.json into the container (alongside config.toml and AGENTS.md), so log in on the host first with codex login. If the host stores credentials in the OS keyring (no auth.json) or you have never logged in, authenticate inside the container with codex login --device-auth (requires device-auth enablement in your org) or codex login --with-api-key - the plain codex login browser flow does not work inside the container because its OAuth localhost callback is unreachable from the host browser.

See the Supported Tools wiki page for detailed configuration, API key setup, and adding new tools.

Features

Core Capabilities

  • Multi-slot support - Run parallel AI coding sessions for the same workspace with full isolation
  • Session resume - Resume conversations with full history and credentials restored (workspace-scoped)
  • Persistent containers - Keep containers alive between sessions (installed tools preserved)
  • Workspace isolation - Each session mounts your project directory
  • Slot isolation - Each parallel slot has its own home directory (files don't leak between slots)
  • Workspace files persist even in ephemeral mode - Only the container is deleted, your work is always saved
  • Container snapshots - Create checkpoints, rollback changes, and branch experiments with full state preservation

Host Integration

  • SSH agent forwarding - Use git-over-SSH inside containers without copying private keys ([ssh] forward_agent = true)
  • Host port publishing - Publish container TCP ports on the host ([ports] pool for identity-mapped agent-usable ports, [[ports.map]] for fixed services): agent-started dev servers become reachable at localhost:<port>, with per-slot deterministic allocation, a pre-launch conflict check, and coi trust gating for untrusted project configs
  • Host socket forwarding - Forward arbitrary host Unix sockets into the container ([[sockets]]) so the host endpoint never enters the container - the building block for credential brokers (mint short-lived tokens on the host, fetch them on demand inside). Untrusted project-config sockets are gated behind coi trust
  • Credential catalog - Copy third-party provider credentials into the container via [[credentials]] entries (config or profile): reference a named catalog bundle (bundle = "ollama") or declare an ad-hoc host/container file pair for anything not yet cataloged. claude/opencode/pi's own credential files come from the same built-in catalog. Ad-hoc entries from an untrusted project .coi/config.toml are gated behind coi trust; catalog references carry the same trust level the built-in tool credentials already have
  • Environment variable forwarding - Selectively forward host env vars by name (forward_env in config)
  • Command-sourced env vars - Mint a fresh secret per session by running a host command at start and injecting its output as an env var ([defaults.env_commands]) - for short-lived API keys/tokens. Trusted-scope config only
  • Host timezone inheritance - Containers automatically inherit the host's timezone (configurable via [timezone] config)
  • Sandbox context file - Auto-injected ~/SANDBOX_CONTEXT.md tells AI tools about their environment (network mode, workspace path, persistence, etc.). Automatically loaded into each tool's native context system: Claude Code via ~/.claude/CLAUDE.md, OpenCode via the instructions field in opencode.json, pi via ~/.pi/agent/APPEND_SYSTEM.md symlink, Codex via ~/.codex/AGENTS.md (opt out with auto_context = false)

Security & Isolation

  • Credential protection - SSH keys, .env files, Git credentials, and environment variables are never exposed unless explicitly mounted
  • Privileged container guard - Refuses to start when security.privileged=true is detected, which defeats all container isolation
  • Security posture verification - coi health checks seccomp, AppArmor, and privilege settings to confirm full isolation
  • Kernel version enforcement - Warns on host kernels below 5.15 that may lack security features for safe isolation
  • Real-time threat detection - Kernel-level nftables monitoring detects reverse shells, C2 connections, data exfiltration, DNS tunneling, and credential scanning
  • Automated response - Auto-pause on HIGH threats, auto-kill on CRITICAL - no manual intervention needed
  • Network isolation - nftables-based restricted/allowlist/open modes block private-network access and exfiltration, with fine-grained egress controls: pin DNS to your own resolver (dns_servers), cap outbound ports globally (allowed_ports) or per-destination (allowed_domains with :ports, and per-host [[network.hosts]] ports) - e.g. "internet open, on the LAN only redmine:443"
  • Protected paths - .git/hooks, .git/config, .husky, .vscode mounted read-only to prevent supply-chain attacks
  • Host-side immutable protection - Protected paths are locked with chattr +i during sessions, preventing unshare -m + umount bypass of read-only mounts (opt out: [security] host_immutable = false)
  • Git identity guard - Containers enforce user.useConfigOnly=true, preventing AI tools from committing as the default "code" user. Pin a fixed identity with [git] name/email, and set [git] readonly = true to mount ~/.gitconfig read-only so the agent can't git config --global over it (locks the whole global config; use --local for other settings)
  • Guest API disabled - Incus guest API (/dev/incus) disabled by default, preventing host path and topology leaks
  • System containers - Full OS isolation with unprivileged containers, better than Docker privileged mode
  • Automatic UID mapping - No permission hell, files owned correctly
  • Audit logging - All security events logged to JSONL for forensics and compliance

Safe Dangerous Operations

  • AI coding tools often need broad filesystem access or bypass permission checks
  • These operations are safe inside containers because the "root" is the container root, not your host system
  • Containers are ephemeral - any changes are contained and don't affect your host
  • This gives AI tools full capabilities while keeping your system protected

Quick Start

# Install
curl -fsSL https://raw.githubusercontent.com/mensfeld/code-on-incus/master/install.sh | bash

# Build image (first time only, ~5-10 minutes)
coi build

# Start coding with your preferred AI tool (defaults to Claude Code)
cd your-project
coi shell

# Or use opencode instead (config-driven: [tool] name = "opencode",
# or a profile: coi shell --profile opencode)

# That's it! Your AI coding assistant is now running in an isolated container with:
# - Your project mounted at /workspace
# - Correct file permissions (no more chown!)
# - Full Docker access inside the container
# - GitHub CLI available for PR/issue management
# - All workspace changes persisted automatically
# - No access to your host SSH keys, env vars, or credentials

Why Incus Instead of Docker or Docker Sandboxes?

Incus is a modern Linux container and virtual machine manager, forked from LXD. Unlike Docker (which uses application containers), Incus provides system containers that behave like lightweight VMs with full init systems.

Security Comparison

Capabilitycode-on-incusDocker SandboxBare Metal
Credential isolationDefault (never exposed)PartialNone
Real-time threat detectionKernel-level (nftables)NoNo
Reverse shell detectionAuto-killNoNo
Data exfiltration alertsAuto-pauseNoNo
Network isolationnftables (3 modes)BasicNo
Protected pathsRead-only mountsNoNo
Auto response (pause/kill)YesNoNo
Audit loggingJSONL forensicsNoNo
Supply-chain attack preventionGit hooks/IDE configs protectedNoNo

Why Incus Instead of Docker Sandboxes?

  • Linux-first, not Linux-last. Docker Sandboxes' microVM isolation is only available on macOS and Windows. Linux gets a legacy container-based fallback. COI is built for Linux from the ground up because Incus is Linux-native.

  • No Docker Desktop required. Docker Sandboxes is a Docker Desktop feature. Docker Desktop is not open source and has commercial licensing requirements for larger organizations. COI depends only on Incus - fully open source, no vendor lock-in, no additional runtime.

  • System containers, not containers-in-VMs. Incus system containers run a full OS with systemd and native Docker support inside - one clean isolation layer. Docker Sandboxes nests application containers inside microVMs, adding architectural complexity.

  • No permission hell. Incus automatic UID/GID shifting means files created by agents have correct ownership on the host. No mapping hacks needed. (Note: files created via sudo in the workspace will be root-owned - the sandbox context file instructs AI tools to fix ownership after sudo operations.)

  • Credential isolation by default. Host environment variables, SSH keys, and Git credentials are never exposed to AI tools unless explicitly mounted.

  • Simple and transparent. No separate daemon, no opaque VM nesting. COI talks directly to Incus - easy to inspect, debug, and extend.

Installation

Automated Installation (Recommended)

# One-shot install
curl -fsSL https://raw.githubusercontent.com/mensfeld/code-on-incus/master/install.sh | bash

# This will:
# - Download and install coi to /usr/local/bin
# - Check for Incus installation
# - Verify you're in incus-admin group
# - Show next steps

Manual installation: Download the binary from GitHub Releases, make it executable, and move to /usr/local/bin/. Requires Linux with Incus installed and user in the incus-admin group. You must log out and back in (or run newgrp incus-admin) after adding your user to the group - COI runs incus directly and requires the group to be active in your session. See the Incus installation guide for setting up Incus.

Build Images

coi build builds the coi-default base image - Ubuntu 24.04 with Docker-in-container, mise-managed runtimes (Python, pnpm, TypeScript, tsx; add more on demand), Node.js LTS, the AI CLIs, gh, and the usual dev tooling (git, tmux, database clients, debugging utilities). Layer your own specialized images on top with a profile [container.build] section and coi build --profile <name>. See the Image Management wiki page for the full build workflow, flags, and custom-image recipes.

macOS Support

COI works on macOS using Colima or Lima VMs. See the macOS Setup Guide for complete instructions.

Usage

Basic Commands

coi shell                 # interactive AI session (Claude Code by default)
coi run -- npm test       # run a command in the sandbox (streams output, propagates exit code)
coi attach                # attach to a running session
coi list --all            # active containers + saved sessions
coi logs / coi audit      # session logs and the JSONL threat-event audit stream
coi monitor               # real-time security dashboard
coi trust                 # approve out-of-workspace mounts/sockets from a project .coi/config.toml
coi shutdown / coi kill   # stop or force-kill containers
coi clean                 # remove stopped containers and orphaned resources
coi update                # update coi to the latest release

Tool selection, slots (--slot), resume (--resume), mounts, limits and network mode are config/profile-driven. See the Container Operations wiki page for the full command reference (or coi <command> --help).

Upgrading to 0.10? 0.10 removes all config-shaped CLI flags (--image, --persistent, --tmux, --tool, coi build --compression, coi shutdown --timeout) and the legacy CLAUDE_ON_INCUS_* / COI_LIMIT_* env-var overrides - everything config-shaped now lives in config files and profiles, and a removed flag fails with a hint naming its replacement key. See the Upgrading from 0.9 to 0.10 guide (the 0.8→0.9 notes are there too).

Container Aliases

Assign human-friendly names to containers for easy management from any directory:

# .coi/config.toml (in your project)
[container]
alias = "myproject"
coi shell myproject              # Launch session using alias (from any directory)
coi attach myproject             # Attach to running aliased container

See the Container Lifecycle and Sessions guide for full alias documentation.

Global Flags

--workspace PATH        # Workspace directory to mount (default: current directory)
--slot NUMBER           # Slot number for parallel sessions (0 = auto-allocate)
--resume [SESSION_ID]   # Resume from session (omit ID to auto-detect latest for workspace)
--continue [SESSION_ID] # Alias for --resume
--profile NAME          # Use named profile

Everything else - image selection, persistence, network mode, mounts, socket forwarding, environment variables, SSH agent, monitoring, timezone, resource limits - is configured via config files or profiles, not flags (the former --image and --persistent flags were removed in 0.10; set [container] image / persistent instead). See the Configuration wiki page for the full reference.

Advanced Usage

See the wiki for detailed documentation:

Run Scripts and Commands in the Sandbox

COI's isolation isn't only for AI agents - coi run executes regular commands and scripts with the same protection: workspace mount, read-only protected paths, secret masking, network isolation, resource/time limits, and security monitoring. Output streams live, stdin is connected, and the command's exit code becomes coi run's exit code.

coi run -- <cmd> runs any command (stdin connected, output streamed), and a bare coi run executes an extensionless, executable ./coi-run script directly from the workspace mount - the shebang picks the interpreter. The container is cleaned up afterwards unless [container] persistent = true.

Security note: a cloned repository can ship its own coi-run, so coi run in a repo you don't trust executes that repo's code - inside the sandbox, which is exactly what the sandbox is for. For untrusted projects, use a credential-limiting profile (e.g. coi run --profile hardened, or your own profile with [ssh] forward_agent = false and a restricted network mode) so the script gets no SSH agent, forwarded env, or open egress.

Session Resume

Resume a previous AI coding session with full history and credentials restored:

coi shell --resume              # Auto-detect latest session for this workspace
coi shell --resume=<session-id> # Resume specific session
coi list --all                  # List available sessions

What's restored: Full conversation history, tool credentials, user settings, and project context. The profile used when the session was created is also automatically restored - no need to pass --profile again (explicitly passing --profile overrides the saved one). Sessions are workspace-scoped - --resume only finds sessions from the current workspace directory.

See the Container Lifecycle and Sessions guide for details on how session persistence works.

Persistent Mode

By default, containers are ephemeral (deleted on exit). Your workspace files always persist regardless of mode.

Enable persistent mode to also keep the container and its installed packages:

# ~/.coi/config.toml, ./.coi/config.toml, or a profile
[container]
persistent = true

What persists:

  • Ephemeral mode: Workspace files + session data (container deleted)
  • Persistent mode: Workspace files + session data + container state + installed packages, system setup

See the Container Lifecycle and Sessions guide for details.

Configuration

Config file: ~/.coi/config.toml

[container]
image = "coi-default"
persistent = true
# storage_pool = ""            # Empty = Incus default pool
# alias = "myproject"          # Human-friendly name for this workspace's containers

[tool]
name = "claude"
permission_mode = "bypass"
# auto_context = true          # Auto-inject sandbox context into tool's native system

Configuration hierarchy (highest precedence last):

  1. Built-in defaults
  2. User config (~/.coi/config.toml, or the file $COI_CONFIG points at)
  3. Project config (./.coi/config.toml)
  4. Profile (--profile <name>)

Config-shaped settings have no CLI flags and no env-var overrides - config and profiles are the single source of truth. The remaining CLI flags are per-invocation choices only: --workspace, --slot, --resume, --profile.

Place a .coi/config.toml in any repository root to auto-configure COI for that project - useful for teams to share container image, environment, and resource limits.

See the Configuration wiki page for the full config reference, per-repo setup, profiles, and environment variables.

Forwarding host sockets, minting secrets & copying credential files

Give containerized tools credentials without exposing your host secrets: [[sockets]] forwards a host Unix socket (the building block for credential brokers that mint short-lived tokens on demand), [defaults.env_commands] injects a host command's output as an env var, [[credentials]] copies credential files (from COI's built-in catalog via bundle = "…", or an ad-hoc host/container path), and [ports] publishes container ports on the host so agent-started services are reachable at localhost:<port>. Untrusted project-config sockets, ports, and ad-hoc credentials are gated behind coi trust; env_commands from an untrusted config is ignored. See the Configuration and Port Publishing wiki pages for full examples and the trust model.

Profiles

Profiles are reusable container configurations bundling image, tool, limits, mounts, sockets, build scripts, context files, and environment into named templates.

coi shell --profile rust-dev                 # Use a profile
coi profile create rust-dev                  # Create a new profile (then edit its config.toml)
coi profile list                             # List all profiles

Each profile is a self-contained directory (.coi/profiles/<name>/) bundling a config.toml plus optional build script and context file. Profiles support inheritance (inherits = "parent"), context files for AI-agent instructions, and custom build scripts. COI also ships a JSON Schema for profile configs (coi schema profile) so external tools can validate them. See the Profiles wiki page for the full reference, examples, and schema details.

Opening an untrusted repo safely

For inspecting code you don't trust, COI ships a built-in hardened profile - a one-flag preset:

coi shell --profile hardened        # restricted net + secret masking + ephemeral + monitoring
coi profile info hardened           # see exactly what it locks down

It bundles COI's strongest controls: network.mode = "restricted" (no exfil path), workspace secret masking (.env, *.pem, secrets/**, …), host immutability, an ephemeral container, no SSH-agent forwarding, and real-time threat monitoring with auto-pause/kill. It overrides a weaker global config (a global mode = "open" still becomes restricted) and needs no setup.

Resource and Time Limits

See the Resource and Time Limits guide for complete documentation on controlling container resource consumption and runtime.

Quick example:

# ~/.coi/config.toml
[limits.cpu]
count = "2"

[limits.memory]
limit = "2GiB"

[limits.runtime]
max_duration = "2h"

CPU, memory/swap, disk I/O, max runtime and process count are all configurable, with auto-stop on time limits - see the wiki page above for the full set.

Container Lifecycle & Session Persistence

See the Container Lifecycle and Sessions guide for detailed explanation of how containers and sessions work.

Key concepts:

  • Workspace files: Always saved (regardless of mode)
  • Session data: Always saved to ~/.coi/sessions-<tool>/
  • Ephemeral mode (default): Container deleted after exit, session preserved
  • Persistent mode ([container] persistent = true in config or a profile): Container kept with all installed packages
  • Resume (--resume): Restore AI conversation in fresh/existing container

The wiki page above covers the full command set (coi persist, coi unfreeze, coi shutdown/coi close, the in-container close alias) and how resume matches sessions.

Network Isolation

See the Network Isolation guide for complete documentation on network security and nftables-based network filtering.

Network modes:

  • Restricted (default) - Blocks private networks, allows internet
  • Allowlist - Only specific domains/IPs allowed
  • Open - No restrictions (trusted projects only)
# ~/.coi/config.toml
[network]
mode = "restricted"   # Default - blocks private networks, allows internet
# mode = "allowlist"  # Only specific domains/IPs allowed
# mode = "open"       # No restrictions (trusted projects only)

Allowlist mode

In allowlist mode the container does not resolve names itself. COI resolves the allowlisted hostnames on the host, installs those addresses in the firewall, and writes the same addresses into the container's /etc/hosts. DNS egress is then blocked, so that hosts file is the container's only way to turn a name into an address.

That equality is the whole point: the container cannot reach an address the firewall has not already been given, because there is nowhere else for an address to come from. Nothing has to stay running for this to hold - it survives coi exiting, detaching from tmux, or the process being killed.

[network]
mode = "allowlist"
allowed_domains = [
    "api.anthropic.com",       # exact hostname
    "registry.npmjs.org",
    "10.0.0.0/8",              # IPv4 CIDR - no name resolution involved
    "8.8.8.8",                 # raw IPv4 address
]

Wildcards are not supported, and are rejected rather than quietly mishandled. Because each name is resolved up front and written to /etc/hosts, there is no answer to write for *.example.com - you cannot know which subdomains will be asked for. List the exact hostnames, or allow the provider's published IP ranges as CIDRs.

Claude via GCP Vertex AI - list the endpoints, which are enumerable:

allowed_domains = [
    "us-central1-aiplatform.googleapis.com",   # your region
    "oauth2.googleapis.com",
    "sts.googleapis.com",
]

Or, for blanket coverage without naming endpoints, use Google's published ranges (from https://www.gstatic.com/ipranges/goog.json) - these need no resolution at all:

allowed_domains = ["142.250.0.0/15", "172.217.0.0/16", "216.239.32.0/19"]

A host outside the allowlist has no address in /etc/hosts and no route through the firewall, so it fails to resolve and fails to connect.

DNS pinning (dns_servers)

In restricted mode you can pin the resolvers the container is allowed to reach on port 53. COI accepts :53 only to the listed addresses and rejects every other off-box DNS query, so a compromised container cannot bypass your resolver - for example by talking straight to 8.8.8.8 or a resolver it hardcoded.

[network]
mode = "restricted"
dns_servers = ["192.168.1.2"]   # e.g. your Pi-hole

The bridge's own resolver (the container's normal DHCP-provided DNS) travels a different path and is left untouched, so ordinary resolution keeps working with no resolv.conf changes. A pinned resolver on your LAN stays reachable on :53 even when block_private_networks is on - but on port 53 only, never on other ports.

  • IPv4 addresses only, and trusted-scope only: a resolver pin from a project ./.coi/config.toml is a DNS-redirect primitive, so it is ignored from untrusted sources.
  • Not valid in allowlist mode, which blocks all DNS by design (COI errors out if you combine them). List the resolver in allowed_domains instead.
  • Caveat: pinning :53 only bites when port 443 is also constrained (allowlist mode, or allowed_ports below). Otherwise malware can still tunnel DNS-over-HTTPS on 443.

Egress port allowlist (allowed_ports)

Restrict which destination ports the container may reach. In restricted mode this caps the otherwise-open internet egress to just these ports; in allowlist mode it further constrains the allowlisted hosts. Everything else is rejected (ICMP echo still works, so ping/health checks are fine).

[network]
mode = "restricted"
allowed_ports = [80, 443]        # web only; blocks SSH, DB ports, IoT panels, ...

This turns "installed a malicious package that now scans the LAN" into a far smaller problem: even reachable hosts are reachable only on the ports they legitimately need, so lateral movement to SSH (22), databases (5432/6379/…), or device admin daemons is cut off.

  • Bridge-provided DNS is unaffected; add 53 to allowed_ports if the container resolves via an off-box resolver.
  • Trusted-scope only (ignored from a project ./.coi/config.toml).
  • Applies to the LAN too. Even with allow_local_network_access = true, the local network is reachable only on these ports - so enabling local access does not silently reopen SSH/DB ports on your LAN. Likewise dns_servers filters :53 everywhere, so a LAN resolver (Pi-hole) must be listed by its exact IP to stay reachable.
  • Combine with dns_servers for the full "use my Pi-hole, on these ports only" posture - the two compose: a pinned resolver is reachable on :53 regardless of allowed_ports.

Per-destination ports (allowed_domains with :ports)

allowed_ports applies one port set to every allowlisted host. When different destinations legitimately need different ports, scope each allowed_domains entry individually with a :ports suffix - a single port, a comma list, or a lo-hi range:

[network]
mode = "allowlist"
allowed_domains = [
    "github.com:443",              # git/HTTPS only
    "registry.npmjs.org:80,443",   # a port list
    "192.168.1.50:8080",           # the NAS web UI - and nothing else on it
    "10.0.0.0/8:22",               # SSH into the lab subnet, but only SSH
    "svc.internal:8000-8100",      # a port range
    "api.anthropic.com",           # no port -> inherits allowed_ports (else all)
]

Each destination is then reachable only on its own ports: the NAS answers on 8080 but not on SSH, GitHub on 443 but nothing else. An entry with no :ports inherits the global allowed_ports (or all ports if none is set), so existing allowlists keep working unchanged. IPv4 only; a malformed port fails the session closed at startup.

Per-host ports ([[network.hosts]] with ports)

A [[network.hosts]] entry can carry its own ports, scoping the firewall reachability of that one host without touching the rest of egress. This is the piece that lets restricted mode open a single LAN service on a single port while the internet stays fully open - which the global allowed_ports alone can't do, because it caps every destination including the internet.

The canonical "internet open, on the LAN only redmine.susanoo.pl:443, and my Pi-hole resolves it to its local address" posture:

[network]
mode        = "restricted"          # all internet open; LAN + metadata blocked by default
dns_servers = ["192.168.1.2"]       # your Pi-hole (reachable on :53 even though the LAN is blocked)

[[network.hosts]]
ip        = "192.168.1.50"          # redmine's LAN address
hostnames = ["redmine.susanoo.pl"]  # written to /etc/hosts, so it resolves to the local IP
ports     = [443]                   # ...and reachable ONLY on 443

Empty ports inherits the global allowed_ports (else all ports). Applies in restricted and allowlist mode; in open mode nothing is blocked so it has no effect. At runtime: coi hosts add <container> <ip> <hostname> --ports 443.

Security Monitoring

COI includes built-in security monitoring to detect and respond to malicious behavior in real-time:

# Enable in config (~/.coi/config.toml)
[monitoring]
enabled = true

Protects against:

  • Reverse shells - Detects common reverse shell patterns (auto-kill)
  • Data exfiltration - Monitors large workspace reads/writes (auto-pause)
  • Environment scanning - Flags processes searching for API keys and secrets
  • Network threats (NFT) - Kernel-level detection of C2 connections, private network access, DNS tunneling, and allowlist violations

Automated response levels:

  • INFO/WARNING: Logged (+ alert for WARNING)
  • HIGH: Container paused (requires coi unfreeze to continue)
  • CRITICAL: Container killed immediately

Audit logs are stored at ~/.coi/audit/<container-name>.jsonl in JSON Lines format.

coi audit streams this log to stdout as JSON Lines (dump, or --follow for live in-container events), ready to pipe into a SIEM or jq. See the Security Monitoring wiki page for monitoring commands, configuration, and NFT setup, and the Audit Log wiki page for the event format, field reference, sources, and tuning.

Security Best Practices

See the Security Best Practices guide for detailed security recommendations.

COI automatically mounts security-sensitive paths as read-only to prevent supply-chain attacks:

  • .git/hooks, .git/config, .husky, .vscode, .coi, .claude/settings.json, .claude/settings.local.json

The .claude/settings.* files can carry auto-executing hooks, so making them read-only stops a contained agent from planting a hook that a later session (or a native run on the host) would auto-execute on open. To opt a path back out, set [security] writable_paths = [".claude/settings.json"] in trusted-scope config (~/.coi/config.toml or $COI_CONFIG) - an untrusted project .coi/config.toml cannot remove protections. ([git] writable_hooks = true remains as a shorthand for .git/hooks.) See the wiki for details.

System Health Check

See the System Health Check guide for detailed information on diagnostics and what's checked.

Run diagnostics:

coi health                    # Basic health check
coi health --format json      # JSON output
coi health --verbose          # Additional checks

It checks system/kernel/Incus setup, permissions, security posture (seccomp/AppArmor/privileged), network, storage, monitoring prerequisites, and running containers, exiting 0 (healthy), 1 (degraded), or 2 (unhealthy).

Troubleshooting

See the Troubleshooting guide for common issues and solutions.

Common issues:

  • DNS issues during build - COI automatically fixes systemd-resolved conflicts
  • Run coi health to diagnose setup problems
  • Check the troubleshooting guide for detailed solutions

Where did the time go?

Set COI_TIMING_DEBUG=1 on any command for a wall-clock breakdown on stderr - every pipeline phase and incus subprocess, nested, with per-category totals (COI_TIMING_DEBUG_JSON=<path> writes JSON instead; scripts/bench-run.py -n 5 reports a median). The usual culprit is a dir storage pool that re-unpacks the whole image every launch (~5-6s/GB) - coi health flags it; recreate the pool on a CoW driver (zfs/btrfs, e.g. by re-running install.sh) to fix it.

Frequently Asked Questions

See the FAQ for answers to common questions.

Topics covered:

  • Orphaned nftables/iptables rules
  • How COI compares to Docker Sandboxes and DevContainers
  • Windows support (WSL2)
  • Security model and prompt injection protection
  • API key security and trust model
  • What is Incus? (vs tmux)

Getting Help

Frequently Asked Questions

What is code-on-incus?

code-on-incus is an open-source ai agents skill for AI coding assistants such as Claude Code, Codex CLI, and ChatGPT, built by mensfeld. Give each AI agent its own isolated machine with root, Docker, and systemd. Active defense detects and stops threats automatically. It has 661 GitHub stars.

Is code-on-incus safe to use?

Yes. code-on-incus passed SkillsLLM's automated security scan — a dependency vulnerability audit plus prompt-injection heuristics — with no high-severity issues. You can read the full report in the Security Report section on this page.

How do I install code-on-incus?

Clone the repository with "git clone https://github.com/mensfeld/code-on-incus" and add it to your Claude Code skills directory (see the Installation section above).

What programming language is code-on-incus written in?

code-on-incus is primarily written in Go. It is open-source under mensfeld on GitHub, so you can review or fork the full source.

Are there alternatives to code-on-incus?

Yes. SkillsLLM lists many other AI Agents skills you can browse and compare side by side. Open the AI Agents category from the badge at the top of this page, or use the Related Skills and comparison links further down to weigh code-on-incus against similar tools.

Comments (0)

No comments yet. Be the first to share your thoughts!

ECC

by affaan-m

10

The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.

242,21936,702JavaScript
AI Agentsai-agentsanthropicclaude-code
View details
15

An agentic skills framework & software development methodology that works.

234,96620,863Shell
AI Agentsai-agentsbrainstorming
View details

The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.

185,94028,768JavaScript
AI Agentsai-agentsanthropicclaude-code
View details

cc-switch

by farion1231

3

A cross-platform desktop All-in-One assistant for Claude Code, Codex, OpenCode, OpenClaw, Grok Build & Hermes Agent. Only official website: ccswitch.io

128,8688,826Rust
AI Agentsclaude-codeai-tools
View details

claude-code

by anthropics

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

120,03119,897Shell
AI Agents
View details

Developers Also Liked

Based on votes and bookmarks from developers who liked this skill

ECC

by affaan-m

10

The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.

242,21936,702JavaScript
AI Agentsai-agentsanthropicclaude-code
View details
15

An agentic skills framework & software development methodology that works.

234,96620,863Shell
AI Agentsai-agentsbrainstorming
View details

n8n

by n8n-io

12

Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.

201,88160,308TypeScript
MCP Serversapisai-tools
View details

The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.

185,94028,768JavaScript
AI Agentsai-agentsanthropicclaude-code
View details

cc-switch

by farion1231

3

A cross-platform desktop All-in-One assistant for Claude Code, Codex, OpenCode, OpenClaw, Grok Build & Hermes Agent. Only official website: ccswitch.io

128,8688,826Rust
AI Agentsclaude-codeai-tools
View details