Configuration - mensfeld/code-on-incus GitHub Wiki
Configuration Files
COI uses TOML configuration files. The primary user config lives at:
~/.coi/config.toml
Create this file manually — no generator command is needed. See the Full Config Reference below for a complete example you can copy and trim down.
Configuration Hierarchy
Settings are loaded in order, with later sources overriding earlier ones:
| Priority | Source | Path |
|---|---|---|
| 1 (lowest) | Built-in defaults | embedded profiles/default/config.toml |
| 2 | User config | ~/.coi/config.toml |
| 3 | Project config | ./.coi/config.toml (repo root) |
| 4 | COI_CONFIG env var |
Path to a custom config file |
| 5 | Environment variables | CLAUDE_ON_INCUS_*, COI_* |
| 6 (highest) | Operational CLI flags | --workspace, --slot, --persistent, --resume, --profile, --image |
Each layer only overrides the fields it explicitly sets. Unset fields inherit from the previous layer.
Per-Repository Configuration
Place a .coi/config.toml file in any repository root to auto-configure COI for that project. When you run coi shell from that directory, the project config is automatically loaded.
Key points:
- Only the fields you set are overridden — everything else inherits from your user and system defaults
- The file uses the same TOML format and sections as the user config
- The
.coi/directory can also contain build scripts, profile directories, and other project assets - Useful for teams: commit
.coi/to the repo so every developer gets the same container image, environment, and resource limits without touching their personal config
Migration from 0.7.x: Project config has moved from
.coi.tomlto.coi/config.toml. COI will refuse to start if a.coi.tomlfile is detected, displaying the migration command:mkdir -p .coi && mv .coi.toml .coi/config.toml.
Example .coi/config.toml
# my-project/.coi/config.toml — project-specific overrides
[container]
image = "coi-rust"
persistent = true
# alias = "my-rust-project"
# storage_pool = ""
[defaults]
forward_env = ["CARGO_REGISTRY_TOKEN"]
[defaults.environment]
RUST_BACKTRACE = "1"
[tool]
name = "claude"
permission_mode = "bypass"
[tool.claude]
effort_level = "high"
[limits.cpu]
count = "4"
[limits.memory]
limit = "4GiB"
[limits.runtime]
max_duration = "4h"
With this file in your repo root and a user config that sets code_uid = 1000 and group = "incus-admin", those user settings still apply — only image, persistent, forward_env, environment, and limits are overridden by the project config.
What you can set in .coi/config.toml
Any section from the full config is valid in a project config:
| Section | Example use case |
|---|---|
[container] |
Image, persistence, alias, storage pool |
[defaults] |
Model, environment variables, forward_env |
[limits.*] |
CPU, memory, disk, and runtime limits |
[tool] |
Default AI tool, permission mode, context file |
[network] |
Network isolation mode and allowed domains |
[mounts] |
Additional mount points |
[git] |
Git hooks write access |
[ssh] |
SSH agent forwarding |
[timezone] |
Container timezone (host, fixed, or UTC) |
[security] |
Protected path overrides |
[monitoring] |
Security monitoring settings |
profiles/ |
Named configuration profiles (as profile directories) |
Full Config Reference
[container]
image = "coi-default"
persistent = false
storage_pool = "" # empty = Incus default pool
# alias = "myproject" # Human-friendly name for this workspace's containers
[container.build]
# base = "coi-default"
# script = "build.sh"
[defaults]
model = "claude-sonnet-4-5"
# Forward host environment variables into the container by name
# Values are read from the host at session start — never stored in config
# forward_env = ["ANTHROPIC_API_KEY", "GITHUB_TOKEN"]
# Static environment variables set inside the container
# [defaults.environment]
# MY_VAR = "value"
[tool]
name = "claude" # AI coding tool: "claude", "opencode"
permission_mode = "bypass" # "bypass" (default) or "interactive"
# binary = "claude" # Optional: override binary name
# Path to a custom context file injected as ~/SANDBOX_CONTEXT.md in every container.
# Supports ~ expansion. If empty, the built-in template is used.
# context_file = "~/my-sandbox-context.md"
# Auto-inject sandbox context into tool's native context system (default: true)
# Claude: writes to ~/.claude/CLAUDE.md; OpenCode: sets instructions field in opencode.json
# auto_context = true
# Claude-specific settings
[tool.claude]
effort_level = "medium" # "low", "medium", or "high"
[paths]
sessions_dir = "~/.coi/sessions"
storage_dir = "~/.coi/storage"
logs_dir = "~/.coi/logs"
# preserve_workspace_path = true # Mount at same path as host instead of /workspace
[incus]
project = "default"
group = "incus-admin"
code_uid = 1000
code_user = "code"
# disable_shift = false # Disable UID shifting (for Colima/Lima environments)
[ssh]
forward_agent = false # Forward host SSH agent (keys stay on host)
[network]
# mode = "restricted" # "restricted" (default), "open", or "allowlist"
# block_private_networks = true
# block_metadata_endpoint = true
# allowed_domains = ["github.com", "pypi.org"] # Only used in "allowlist" mode
# refresh_interval_minutes = 30 # Max DNS refresh interval (actual uses DNS TTL if shorter)
# allow_local_network_access = false # Allow connections from entire local network
[network.logging]
# enabled = true # Network logging enabled by default
# path = "~/.coi/logs/network.log" # Default log path
[mounts]
# Default mounts applied to all sessions
# [mounts.default](/mensfeld/code-on-incus/wiki/mounts.default)
# host = "~/.aws"
# container = "/home/code/.aws"
# readonly = false # Set to true for read-only mount
# See "Mounting Additional Files" section below for common mount patterns
[limits.cpu]
count = "" # "2", "0-3", "0,1,3" or "" for unlimited
allowance = "" # "50%", "25ms/100ms" or "" for unlimited
priority = 0 # 0-10
[limits.memory]
limit = "" # "512MiB", "2GiB", "50%" or "" for unlimited
enforce = "soft" # "hard" or "soft"
swap = "true" # "true", "false", or size like "1GiB"
[limits.disk]
read = "" # "10MiB/s", "1000iops" or "" for unlimited
write = "" # "5MiB/s" or "" for unlimited
max = "" # Combined read+write limit
priority = 0
tmpfs_size = "" # "" = disk-backed /tmp (default), "4GiB" = RAM-backed
[limits.runtime]
max_duration = "" # "2h", "30m" or "" for unlimited
max_processes = 0 # 0 for unlimited
auto_stop = true
stop_graceful = true
[git]
writable_hooks = false # Allow container to write .git/hooks
[security]
# host_immutable = true # Apply chattr +i to protected paths (default: true)
# protected_paths = [".git/hooks", ".git/config", ".husky", ".vscode"]
# additional_protected_paths = [".idea", "Makefile"]
# disable_protection = false
[timezone]
mode = "host" # "host" (inherit from host, default), "fixed", or "utc"
# name = "Europe/Warsaw" # IANA timezone name, only used when mode = "fixed"
[monitoring]
# enabled = false # Enable background security monitoring
# auto_pause_on_high = true # Pause container on high-severity threats
# auto_kill_on_critical = true # Kill container on critical threats
# poll_interval_sec = 2
# file_read_threshold_mb = 50.0 # Alert if >N MB read in poll interval
# file_read_rate_mb_per_sec = 10.0 # Alert on sustained read rate
# audit_log_retention_days = 30
[monitoring.nft]
# enabled = false # Enable nftables network monitoring
# rate_limit_per_second = 100
# dns_query_threshold = 100 # Alert if >N DNS queries/min
# log_dns_queries = true
# lima_host = "" # Lima host for macOS (e.g., "lima-default")
# === Profiles ===
# Profiles are self-contained directories under profiles/.
# See the Profiles wiki page for full documentation.
#
# Directory structure:
# .coi/profiles/rust-dev/config.toml
# .coi/profiles/python-ml/config.toml
#
# Use: coi shell --profile rust-dev
# List: coi profile list
# Show: coi profile info rust-dev
Environment Variables
These environment variables override config file values (but are overridden by operational CLI flags):
| Variable | Effect |
|---|---|
COI_CONFIG |
Path to a custom config file (loaded after project config) |
CLAUDE_ON_INCUS_IMAGE |
Override default image |
CLAUDE_ON_INCUS_SESSIONS_DIR |
Override sessions directory |
CLAUDE_ON_INCUS_STORAGE_DIR |
Override storage directory |
CLAUDE_ON_INCUS_PERSISTENT |
Set true or 1 for persistent mode |
COI_LIMIT_CPU |
CPU count limit |
COI_LIMIT_CPU_ALLOWANCE |
CPU allowance limit |
COI_LIMIT_MEMORY |
Memory limit |
COI_LIMIT_MEMORY_SWAP |
Swap limit |
COI_LIMIT_DISK_READ |
Disk read rate limit |
COI_LIMIT_DISK_WRITE |
Disk write rate limit |
COI_LIMIT_DURATION |
Maximum runtime duration |
CLI Flags
Only operational flags remain on the CLI. All container customization (network, mounts, environment, SSH, monitoring, timezone, resource limits, etc.) is now configured exclusively via config files or profiles.
Kept operational flags:
coi shell \
--workspace /path/to/project \
--slot 2 \
--persistent \
--resume \
--profile rust-dev \
--image coi-custom
Shell-only flags:
coi shell --debug # Launch bash instead of AI tool (for debugging)
coi shell --background # Run in background
coi shell --tmux # Attach to tmux session
coi shell --container NAME # Target specific container
coi shell --tool opencode # Select AI tool
Sandbox Context File
COI automatically injects a ~/SANDBOX_CONTEXT.md file into every container describing the sandbox environment, and by default injects it into each tool's native context-loading mechanism (auto_context = true).
See the Supported Tools — Sandbox Context File wiki page for full details on auto-context injection, per-tool behavior, disabling it, and using a custom context file.
Mounting Additional Files
Use [mounts.default](/mensfeld/code-on-incus/wiki/mounts.default) to bring additional host directories into the container. The key rule is: mount subdirectories, not the parent config directory — mounting the entire config directory (e.g., ~/.config/opencode or ~/.claude) will conflict with COI's own config setup and can wipe your config files.
# Mount specific subdirectories — NOT the parent config dir
[mounts.default](/mensfeld/code-on-incus/wiki/mounts.default)
host = "~/.config/opencode/agents"
container = "/home/code/.config/opencode/agents"
readonly = true
Mounting Claude Code skills, commands, and plugins:
COI copies essential Claude config files (credentials, settings) automatically, but custom subdirectories like skills, commands, and plugins are not included by default. Mount them individually as read-only for security:
[mounts.default](/mensfeld/code-on-incus/wiki/mounts.default)
host = "~/.claude/skills"
container = "/home/code/.claude/skills"
readonly = true
[mounts.default](/mensfeld/code-on-incus/wiki/mounts.default)
host = "~/.claude/commands"
container = "/home/code/.claude/commands"
readonly = true
[mounts.default](/mensfeld/code-on-incus/wiki/mounts.default)
host = "~/.claude/plugins"
container = "/home/code/.claude/plugins"
readonly = true
Important: Always use readonly = true for these mounts to prevent the AI tool from modifying your host configuration. See issue #260 for background on this pattern.
Common use cases:
| What to mount | Host path | Container path |
|---|---|---|
| opencode custom agents | ~/.config/opencode/agents |
/home/code/.config/opencode/agents |
| opencode global AGENTS.md | ~/.config/opencode/AGENTS.md |
/home/code/.config/opencode/AGENTS.md |
| Shared coding standards | ~/my-standards |
/home/code/standards |
Do not mount the entire ~/.config/opencode or ~/.claude directory — COI manages those automatically and mounting over them will cause config files to be wiped on both host and container.
Profiles
Profiles are self-contained directories that bundle image, tool, limits, mounts, build scripts, context files, and environment into named templates. Use coi shell --profile NAME to activate one.
See the Profiles wiki page for complete documentation including config reference, inheritance, context files, build scripts, and the create/edit/delete commands.