Profiles - mensfeld/code-on-incus GitHub Wiki
Profiles are reusable, self-contained container configurations. Each profile bundles image, tool settings, limits, mounts, build scripts, environment variables, network config, and optional context files into a named directory.
Each profile is a directory under profiles/ containing a config.toml and optional supporting files:
.coi/
├── config.toml # project config
└── profiles/
├── rust-dev/
│ ├── config.toml # profile config
│ ├── build.sh # profile-specific build script
│ └── CONTEXT.md # AI agent context (appended to sandbox context)
└── python-ml/
├── config.toml
└── setup.sh
Profile directories are scanned at two config levels:
| Priority | Location |
|---|---|
| 1 (lowest) |
~/.coi/profiles/NAME/config.toml (user) |
| 2 (highest) |
./.coi/profiles/NAME/config.toml (project) |
Profiles from all discovered locations are merged into a single namespace. If the same profile name is defined in more than one location, COI refuses to start and asks you to rename one so it's always unambiguous which profile is being applied. Operational CLI flags (--persistent, --slot, etc.) always override profile settings.
A profile config.toml uses the same sections as the main config. All fields are optional — only set what you want to override.
# .coi/profiles/rust-dev/config.toml
context = "CONTEXT.md" # context file (see below)
forward_env = ["CARGO_REGISTRY_TOKEN"]
[container]
image = "coi-rust"
persistent = true
[container.build]
base = "coi-default"
script = "build.sh" # resolved relative to this config.toml
[environment]
RUST_BACKTRACE = "1"
[tool]
name = "claude"
permission_mode = "bypass"
[tool.claude]
effort_level = "high"
[[mounts]]
host = "~/.cargo"
container = "/home/code/.cargo"
[network]
mode = "restricted"
# allowed_domains = ["crates.io", "github.com"]
[limits.cpu]
count = "4"
[limits.memory]
limit = "4GiB"
[limits.runtime]
max_duration = "4h"| Field | Type | Description |
|---|---|---|
context |
string | Path to context file (see Context Files) |
forward_env |
string[] | Host env vars to forward into the container |
[container] |
section | Container settings (image, persistent, storage_pool, alias) |
[container.build] |
section | Custom image build (base, script, commands) |
[environment] |
map | Static environment variables |
[tool] |
section | AI tool config (name, binary, permission_mode, context_file, auto_context) |
[tool.claude] |
section | Claude-specific settings (effort_level) |
[[mounts]] |
array | Additional mount points (host, container, readonly). Note: In profiles, use [[mounts]]; in the main config, use [[mounts.default]]
|
[network] |
section | Network isolation (mode, allowed_domains) |
[limits.cpu] |
section | CPU limits (count, allowance, priority) |
[limits.memory] |
section | Memory limits (limit, enforce, swap) |
[limits.disk] |
section | Disk I/O limits (read, write, max) |
[limits.runtime] |
section | Runtime limits (max_duration, max_processes) |
inherits |
string | Parent profile name for inheritance (see Inheritance) |
model |
string | Override default AI model |
[paths] |
section | Path overrides (sessions_dir, storage_dir, logs_dir, preserve_workspace_path) |
[incus] |
section | Incus settings (project, group, code_uid, code_user) |
[git] |
section | Git settings (writable_hooks) |
[ssh] |
section | SSH settings (forward_agent) |
[security] |
section | Security settings (host_immutable, protected_paths) |
[monitoring] |
section | Security monitoring settings |
[timezone] |
section | Timezone settings (mode, name) |
Profiles can inherit from a parent using inherits = "parent-name",
so you only override what differs:
# .coi/profiles/rust-dev-nightly/config.toml
inherits = "rust-dev"
[container]
image = "coi-rust-nightly"
[environment]
RUST_CHANNEL = "nightly"Merge strategy:
- Environment maps deep-merge (child keys win; set to
""to clear a parent key) - Arrays (
mounts,forward_env) fully replace if the child defines them - Scalars override if set by the child
- Struct sections (
limits,tool,build,network) deep-merge field by field
Inheritance works across config levels (a project profile can inherit from a
user-level profile), supports chains up to 10 levels, and has cycle detection.
The built-in default profile can be used as a parent via inherits = "default".
Profiles can include a context file — a markdown file with AI-agent-specific instructions that gets automatically appended to the sandbox context when the profile is used.
# .coi/profiles/python-ml/config.toml
context = "CONTEXT.md"<!-- .coi/profiles/python-ml/CONTEXT.md -->
## Python ML Project Guidelines
- Use pytest for all testing
- Follow PEP 8 style conventions
- Use type hints for all function signatures
- Prefer numpy vectorized operations over loopsWhen coi shell --profile python-ml is used:
- The context file path is resolved relative to the profile directory (absolute and
~paths also work) - The file is validated — if it doesn't exist, the session fails with a clear error
- The content is appended to
~/SANDBOX_CONTEXT.mdunder a# User-Provided Profile Contextheading - The content is also appended to tool-native auto-context files (e.g.,
~/.claude/CLAUDE.mdfor Claude Code)
This means the AI agent automatically receives your profile-specific instructions on top of the standard sandbox environment info — no manual setup needed.
The context file can be named anything (not just CONTEXT.md). The context field just needs to point to a valid file:
context = "instructions.md"
context = "AI_GUIDELINES.md"
context = "../shared/common-context.md" # relative paths work
context = "~/global-context.md" # tilde expansion worksProfiles can specify a build script that runs on top of a base image to create a custom container image:
[container.build]
base = "coi-default" # base image to build on
script = "build.sh" # script path (relative to profile dir)The script is resolved relative to the profile directory. Example build script:
#!/bin/bash
# .coi/profiles/rust-dev/build.sh
apt-get update && apt-get install -y rustup
rustup default stableYou can also use inline commands instead of a script:
[container.build]
base = "coi-default"
commands = ["apt-get update", "apt-get install -y rustup"]coi profile listShows all loaded profiles in a table:
NAME IMAGE PERSISTENT SOURCE
python-ml coi-default - .coi/profiles/python-ml/config.toml
rust-dev coi-rust true .coi/profiles/rust-dev/config.toml
limited coi-default false ~/.coi/profiles/limited/config.toml
coi profile info rust-devDisplays the full configuration of a named profile:
Profile: rust-dev
Source: .coi/profiles/rust-dev/config.toml
context = "/path/to/.coi/profiles/rust-dev/CONTEXT.md"
forward_env = ["CARGO_REGISTRY_TOKEN"]
[container]
image = "coi-rust"
persistent = true
[container.build]
base = "coi-default"
script = "/path/to/.coi/profiles/rust-dev/build.sh"
[environment]
RUST_BACKTRACE = "1"
[tool]
name = "claude"
permission_mode = "bypass"
[tool.claude]
effort_level = "high"
[limits.cpu]
count = "4"
coi shell --profile rust-devProfile settings are merged into the running config. CLI flags override profile values:
# Uses rust-dev profile but overrides persistence
coi shell --profile rust-dev --persistentcoi profile create rust-dev --image coi-rust --inherits default
coi profile create limited --persistent --projectFlags:
-
--image <alias>— set the profile's image -
--inherits <parent>— set a parent profile to inherit from -
--persistent— setpersistent = true -
--user— force creation in~/.coi/profiles/ -
--project— force creation in./.coi/profiles/
Without --user/--project, the location auto-detects: project .coi/ if it exists, otherwise ~/.coi/.
coi profile edit rust-dev
EDITOR=nano coi profile edit rust-devOpens the profile's config.toml in $VISUAL, $EDITOR, or vi. After the editor exits, the file is re-parsed and validated — warnings are printed on invalid TOML but the file is not deleted.
coi profile delete rust-dev # Interactive confirmation
coi profile delete old-profile --force # Skip confirmationThe built-in default profile cannot be edited or deleted.
# .coi/profiles/quick/config.toml
[container]
persistent = false
[limits.runtime]
max_duration = "30m"# .coi/profiles/fullstack/config.toml
forward_env = ["GITHUB_TOKEN", "DATABASE_URL"]
[container]
image = "coi-default"
persistent = true
[environment]
NODE_ENV = "development"
[tool]
name = "claude"
permission_mode = "bypass"
[[mounts]]
host = "~/.npm"
container = "/home/code/.npm"
[limits.cpu]
count = "4"
[limits.memory]
limit = "8GiB"# .coi/profiles/research/config.toml
context = "CONTEXT.md"
[tool]
name = "claude"
permission_mode = "interactive"
[network]
mode = "allowlist"
allowed_domains = ["github.com", "stackoverflow.com", "docs.python.org"]
[limits.runtime]
max_duration = "1h"
[limits.memory]
limit = "2GiB"- Profile directories without a
config.tomlare silently skipped - Invalid TOML in a profile
config.tomlcauses a fatal error at config load time - Missing context files are validated at
--profileusage time (not at config load) — this allows profiles to be loaded from all sources without requiring every referenced file to exist - Missing build scripts are validated at
--profileusage time -
coi profile infodisplays all resolved paths (build scripts, context files) so you can verify they're correct