Skip to content

Configuration

Maciej Mensfeld edited this page Jul 29, 2026 · 24 revisions

Configuration

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 (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.

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.toml to .coi/config.toml. COI will refuse to start if a .coi.toml file 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, 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)

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
# 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-dev

Environment Variables

There 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.

CLI Flags

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-dev

Shell-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 container

Removed 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>

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]] to bring additional host directories into the container.

Warning: Always mount subdirectories, not the parent config directory. Mounting ~/.claude or ~/.config/opencode directly will conflict with COI's own config injection and can silently overwrite your credentials and settings. Mount ~/.claude/skills or ~/.config/opencode/agents instead.

# Mount specific subdirectories - NOT the parent config dir
[[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]]
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 = 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.


See Also

Clone this wiki locally