-
Notifications
You must be signed in to change notification settings - Fork 51
Configuration
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.
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 (or the file $COI_CONFIG points at) |
| 3 | Project config |
./.coi/config.toml (repo root) |
| 4 (highest) | Profile | selected with coi shell/run --profile <name>
|
Each layer only overrides the fields it explicitly sets. Unset fields inherit from the previous layer.
There is no environment-variable config layer (as of v0.10.0): the legacy CLAUDE_ON_INCUS_* and COI_LIMIT_* overrides were removed — they silently outranked explicit config. $COI_CONFIG itself and test/debug toggles still exist, but they select or diagnose, they don't configure. CLI flags are operational only (--workspace, --slot, --resume/--continue, --profile) and never carry 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.
# 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.
Any section from the full config is valid in a project config:
| Section | Example use case |
|---|---|
[container] |
Image, persistence, alias, storage pool, shutdown/readiness timeouts |
[defaults] |
Environment variables, forward_env, and profile — the profile a no-flag coi uses (profile is trusted-scope only; ignored with a warning from a project config). In profiles, forward_env is a top-level key, not under [defaults]. |
[limits.*] |
CPU, memory, disk, and runtime limits |
[tool] |
Default AI tool, permission mode, context file |
[network] |
Network isolation mode and allowed domains (but [[network.hosts]] is trusted-scope config only — ignored from project config) |
[mounts] |
Additional mount points (out-of-workspace mounts gated behind coi trust) |
[[sockets]] |
Forward host Unix sockets into the container (gated behind coi trust) |
[ports] |
Publish container TCP ports on the host — pool + [[ports.map]] (gated behind coi trust; see Port Publishing) |
[[credentials]] |
Copy credential files from host into the container (ad-hoc entries gated behind coi trust) |
[defaults.env_commands] |
Mint env vars from host commands — trusted-scope config only (ignored from project config) |
[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) |
[container]
image = "coi-default"
persistent = false
storage_pool = "" # empty = Incus default pool
# alias = "myproject" # Human-friendly name for this workspace's containers
# shutdown_timeout = 60 # Seconds to wait for graceful shutdown before force-killing (default: 60)
# ready_timeout = 30 # Seconds to wait for a launched container to become ready (default: 30)
# # Raise on slow hosts (nested virt, cold storage pools)
[container.build]
# base = "coi-default"
# script = "build.sh"
[defaults]
# Profile a bare `coi` (no --profile) launches into. Lowest-precedence profile
# source: an explicit --profile, an alias, and a --resume-remembered profile all
# win over it. Honored ONLY from trusted-scope config (~/.coi/config.toml /
# $COI_CONFIG); ignored (with a warning) from a project ./.coi/config.toml.
# An unknown name is a hard error only when `coi shell`/`coi run` applies it.
# profile = "pickles"
# 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"
# Command-sourced env vars: run a host command at session start and inject its
# trimmed stdout as the value — for minting short-lived secrets per session.
# Honored ONLY from trusted-scope config (~/.coi/config.toml / $COI_CONFIG);
# ignored (with a warning) from an untrusted project ./.coi/config.toml.
# [defaults.env_commands]
# AWS_BEARER_TOKEN_BEDROCK = "~/bin/mint-bedrock-key.sh"
# env_command_timeout = "30s" # per-command timeout (default 30s)
[tool]
name = "claude" # AI coding tool: "claude", "opencode", "pi"
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]
# model = "opus" # Claude model, delivered as ANTHROPIC_MODEL: an alias
# # like "opus" or a full ID like "claude-opus-4-8".
# # Unset (default): Claude Code uses its own default.
effort_level = "medium" # "low", "medium", "high", "xhigh", "max", or "auto"
# unset (default): user controls it interactively
[shell]
# use_tmux = true # false = run the tool directly without a tmux
# # session (was the removed `--tmux` flag)
[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
# use_sudo = true # false = never invoke sudo for nft/iptables (see "Running without sudo")
# Static /etc/hosts entries with mode-aware firewall reachability (v0.11.0).
# TRUSTED-SCOPE ONLY: entries in a project ./.coi/config.toml are ignored with a
# warning. See the Network Isolation wiki page for the per-mode reachability rules.
# [[network.hosts]]
# ip = "10.0.0.5" # IPv4 address
# hostnames = ["db.internal", "db"] # one or more names for that address
[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]]
# 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
# Forward host Unix sockets into the container via an Incus proxy device, so the
# host endpoint never enters the container (the building block for credential
# brokers). Sockets from an untrusted project ./.coi/config.toml are gated behind
# `coi trust`, like out-of-workspace mounts.
# [[sockets]]
# host = "~/.coi/broker.sock" # host socket (~ expanded)
# container = "/run/broker.sock" # absolute path inside the container
# env = "AWS_BROKER_SOCK" # optional: env var set to the container path
# Publish container TCP ports on the host via an Incus proxy device (the same
# primitive as [[sockets]], pointed the other way), so agent-started services
# are reachable at localhost:<port> (as of v0.10.1). Loopback-only by default;
# gated behind `coi trust` from an untrusted project ./.coi/config.toml.
# See the Port Publishing wiki page for allocation, preflight, and trust details.
# [ports]
# pool = 3 # identity-mapped ports per session (0-10):
# # host port == container port; exported as COI_PORTS,
# # and the sandbox context tells the agent to use them
# [[ports.map]]
# name = "web" # unique; exported as COI_PORT_WEB
# container = 3000 # TCP port inside the container
# host = 13000 # optional exact host pin (omit = auto per workspace/slot)
# listen = "127.0.0.1" # optional host listen address (default loopback;
# # "0.0.0.0" opts into LAN exposure)
# Copy credential FILES from host into the container at session setup (as of
# v0.10.0) — for tools that read credentials from disk rather than an env var
# or a socket. Files are pushed (not mounted), chowned to the container user,
# and optionally chmod'd; a missing host file is skipped with a log line.
# Reference a named bundle from COI's built-in catalog (the same catalog
# claude/opencode/pi use for their own credentials):
# [[credentials]]
# bundle = "ollama" # copies ~/.ollama/id_ed25519, mode 0600
# ...or declare an ad-hoc host/container pair for anything not yet cataloged.
# Ad-hoc entries from an untrusted project ./.coi/config.toml are gated
# behind `coi trust` (bundle references are never gated — the host path comes
# from COI's own catalog, not the referencing config):
# [[credentials]]
# host = "~/.config/some-tool/token.json" # host file (~ expanded)
# container = "/home/code/.config/some-tool/token.json" # absolute in-container path
# mode = "0600" # optional chmod after copy
[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 = "" # bytes/sec: "10MiB", "1000iops" or "" for unlimited (no "/s" suffix)
write = "" # bytes/sec: "5MiB" or "" for unlimited (no "/s" suffix)
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", ".git/config.worktree", ".git/info/attributes", ".husky", ".vscode", ".coi", ".claude/settings.json", ".claude/settings.local.json"]
# 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-devThere is no env-var config layer (as of v0.10.0). The legacy CLAUDE_ON_INCUS_* and COI_LIMIT_* overrides were removed entirely — a stray value in a shell profile could silently defeat explicit config. Their replacements are ordinary config keys:
| Removed variable | Config replacement |
|---|---|
CLAUDE_ON_INCUS_IMAGE |
[container] image |
CLAUDE_ON_INCUS_PERSISTENT |
[container] persistent |
CLAUDE_ON_INCUS_SESSIONS_DIR / _STORAGE_DIR
|
[paths] sessions_dir / storage_dir
|
COI_LIMIT_CPU / _CPU_ALLOWANCE
|
[limits.cpu] count / allowance
|
COI_LIMIT_MEMORY / _MEMORY_SWAP
|
[limits.memory] limit / swap
|
COI_LIMIT_DISK_READ / _DISK_WRITE
|
[limits.disk] read / write
|
COI_LIMIT_DURATION |
[limits.runtime] max_duration |
COI_CONFIG still exists — it selects which user config file to load instead of ~/.coi/config.toml; it doesn't configure anything itself.
Two diagnostic env vars also exist and, like COI_CONFIG, never configure anything — they only observe: COI_TIMING_DEBUG=1 prints a startup timing breakdown to stderr at exit, and COI_TIMING_DEBUG_JSON=<path> writes the same data as JSON. See Profiling Slow Startup in Troubleshooting.
Only operational, per-invocation flags remain on the CLI (as of v0.10.0). Everything config-shaped — image, persistence, tool selection, tmux, network, mounts, environment, SSH, monitoring, timezone, resource limits — lives exclusively in config files or profiles. Using a removed flag fails with a migration hint naming the exact replacement key.
Operational flags (global):
coi shell \
--workspace /path/to/project \
--slot 2 \
--resume \
--profile rust-devShell-only flags:
coi shell --debug # Launch bash instead of AI tool (for debugging)
coi shell --background # Run in background
coi shell --container NAME # Target specific containerRemoved flags and their config keys:
| Removed flag | Config replacement |
|---|---|
--image |
[container] image = "..." |
--persistent |
[container] persistent = true |
--tmux / --tmux=false
|
[shell] use_tmux = true/false |
--tool <name> |
[tool] name = "..." (a per-tool profile also carries the tool's whole setup) |
coi build --compression |
[container.build] compression = "..." |
coi shutdown --timeout |
[container] shutdown_timeout = <seconds> |
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.
Use [[mounts.default]] to bring additional host directories into the container.
Warning: Always mount subdirectories, not the parent config directory. Mounting
~/.claudeor~/.config/opencodedirectly will conflict with COI's own config injection and can silently overwrite your credentials and settings. Mount~/.claude/skillsor~/.config/opencode/agentsinstead.
# Mount specific subdirectories - NOT the parent config dir
[[mounts.default]]
host = "~/.config/opencode/agents"
container = "/home/code/.config/opencode/agents"
readonly = trueMounting 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]]
host = "~/.claude/skills"
container = "/home/code/.claude/skills"
readonly = true
[[mounts.default]]
host = "~/.claude/commands"
container = "/home/code/.claude/commands"
readonly = true
[[mounts.default]]
host = "~/.claude/plugins"
container = "/home/code/.claude/plugins"
readonly = trueImportant: 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 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.
- Profiles - Reusable config bundles that extend per-project settings
- Network Isolation - Configuring restricted, allowlist, and open network modes
- Supported Tools - Tool-specific configuration options
- Container Lifecycle and Sessions - How containers and sessions use your config
- Security Best Practices - Recommended security settings
Getting Started
Setup
Configuration & Usage
- Best Practices
- Configuration
- Profiles
- Supported Tools
- Container Lifecycle & Sessions
- Container Operations
- Snapshot Management
- File Transfer
- Port Publishing
- Tmux Automation
- Image Management
- Resource & Time Limits
Security
Maintenance
Help & Reference