Skip to content

Configuration Reference

File location

Source Precedence
--config <file> 1
JAILOC_CONFIG=<file> 2
~/.config/jailoc/config.toml 3 (default)

~ is expanded and relative paths are resolved against the current directory. The default file is auto-created with defaults on first run; a file set with --config or JAILOC_CONFIG must exist.

Per-workspace files, such as the CA bundle copied by ca_bundle, live in workspaces/<name>/ next to the configuration file.

VM state under ~/.cache/jailoc-vm/ is keyed by workspace name only and is shared by all configuration files: workspaces with the same name in two files control the same VM.

All fields are optional unless noted. jailoc ignores settings it does not know and prints a warning naming each of them; jailoc migrate removes them when it rewrites the file.


Top-level fields

Besides the tables [vm], [base], [defaults] and [workspaces.<name>], the configuration has one setting at the root level. Put it before the first table.

Field Type Default Description
detach_keys string "ctrl-b,d" Key sequence that detaches the terminal from an agent session. The --detach-keys flag of jailoc overrides it.

detach_keys is a comma-separated list of keys, typed one after another, in the format of docker attach --detach-keys. A key is ctrl-<letter> (lowercase), one of ctrl-@, ctrl-[, ctrl-\, ctrl-], ctrl-^, ctrl-_, DEL, or a single character other than a comma. While a session is attached, jailoc holds back input that starts like the sequence until the next key decides. Typing the first key twice sends it once to the agent, as in tmux. Keys match also when the agent switches the terminal to an extended keyboard mode (xterm modifyOtherKeys or the kitty keyboard protocol): key release and repeat events, presses of a modifier key alone, and Caps Lock or Num Lock do not break the sequence.

detach_keys = "ctrl-a,d"

The jailoc v1 settings mode and password_mode are rejected at config load time. See jailoc v1 settings.


[vm]

Host runtime of the workspace VMs. All fields are optional.

Field Type Default Description
runner string (embedded) Path to a jailoc-vm-runner (or go-microvm-runner) binary. When empty, jailoc unpacks the VM runtime (jailoc-vm-runner, libkrun, libkrunfw) embedded in release builds for darwin/arm64, linux/amd64 and linux/arm64 into ~/.cache/jailoc-vm/runtime/ on first use. Builds without an embedded runtime (other platforms, builds from source) require runner and lib_dir. Must be absolute; supports ~ expansion.
lib_dir string (none) Directory containing libkrun and libkrunfw for a configured runner. Ignored when runner is empty. When empty with a configured runner, jailoc uses /opt/homebrew/lib on macOS if it exists, otherwise the system library path. Must be absolute; supports ~ expansion.
builder string "auto" Where workspace images are built: "auto" uses the host Docker daemon when it answers and runs Linux on the host's CPU architecture, otherwise the shared builder VM; "docker" requires the host daemon; "vm" always uses the shared builder VM. A workspace with isolated_build = true always uses its own builder VM.

Example

[vm]
runner = "~/.local/bin/go-microvm-runner"
lib_dir = "/opt/homebrew/lib"

[base]

Global base image settings. Controls the base image used when no workspace or default image is set. See Image Resolution.

Field Type Default Description
dockerfile string (none) Local path (/..., ~/...) or HTTP(S) URL to a Dockerfile for the base image. When set, takes priority over the embedded Dockerfile. Build failure is fatal. Maximum file size for HTTP sources: 1 MiB.

Example

[base]
dockerfile = "https://example.com/Dockerfiles/custom"

[defaults]

Defaults applied to all workspaces.

Field Type Default Description
backend string "vm" Accepted values: "vm" or empty. "docker" is rejected as a jailoc v1 setting.
agent string "opencode" Coding agent of the workspaces: "opencode", "opencode1", "opencode2", "claude", "copilot", "omo", "agy" or a custom agent. See Coding agents.
agents table {} Settings per coding agent and custom agent definitions, keyed by agent name. See agents.
image string (none) Pre-built image used as the base for workspace Dockerfile builds, or used directly when no workspace dockerfile is set.
env string[] [] Environment variables for every session, in KEY=VALUE format.
env_file string[] [] .env files loaded for all workspaces. Each file must exist at config load time.
mounts string[] [] Additional directory shares, host:container[:mode]. See mounts.
snapshots string[] [] Read-only snapshots of host files or directories. See snapshots.
allowed_hosts string[] [] Hostnames whose A and AAAA records are allowed through the network broker even when they resolve to private addresses.
allowed_networks string[] [] IPv4 and IPv6 CIDR ranges allowed through the network broker.
ports (int | string)[] [] Guest ports published on host 127.0.0.1. See ports.
host_ports (int | string)[] [] Host 127.0.0.1 ports or Unix sockets reachable from the guest. See host_ports.
ssh_auth_sock bool false Relay the host SSH agent ($SSH_AUTH_SOCK) into the VM at /run/ssh-agent.sock and copy ~/.ssh/known_hosts into it.
gpg_agent bool false Relay the host gpg-agent extra socket into the VM at /run/user/<uid>/gnupg/S.gpg-agent and import the public key of user.signingKey, or of all host secret keys when it is unset, into the VM keyring. Requires gpg in the image.
git_config bool true Copy the host Git configuration read-only into the VM: ~/.gitconfig, config, ignore and attributes from $XDG_CONFIG_HOME/git (default ~/.config/git), and the files they reference under the home directory. See git_config.
import_agent_config bool true Copy the coding agent's host configuration read-only into the VM, such as ~/.claude/CLAUDE.md or ~/.config/opencode. See import_agent_config.
cpu float 2.0 vCPUs of the workspace VM, rounded up.
memory string "4g" Memory of the workspace VM, at least 512m.
docker_disk string "20g" Size of the Docker data disk of an enable_docker workspace and of its isolated_build builder VM, at least 1g. The disk can grow but not shrink.
timezone string (host zone) Time zone of the workspace VM, an IANA name such as "UTC" or "Europe/Prague". Omitted: the host's local zone. See timezone.
enable_docker bool false Run a Docker daemon inside the workspace VM. See enable_docker.
protect_git bool false Make .git/hooks, .git/config, config.worktree, linked worktree commondir pointers, core.hooksPath and included config files read-only inside the VM for repositories under workspace paths and read-write mounts, including symlinked workspace roots. Disable sudo and Docker there.
isolated_build bool false Build the workspace image in a builder VM of its own, with its own build cache and the workspace's network policy, instead of the host Docker daemon or the shared builder VM.
network table {} IPv6 and DNS settings of the network broker. See [defaults.network].
ca_bundle bool | string true CA bundle added to the VM's system trust store. See ca_bundle.
secrets table {} Secrets for all workspaces. See secrets.

Example

[defaults]
agent = "claude"
image = "myregistry.example.com/myteam/agent-base:v1.2.3"
env = ["GOPRIVATE=*.example.com"]
env_file = ["~/.config/jailoc/shared.env"]
allowed_hosts = ["internal-registry.example.com"]
allowed_networks = ["10.0.0.0/8", "fd12:3456::/48"]
ssh_auth_sock = true
cpu = 4.0
memory = "8g"

[defaults.agents.claude]
args = ["--model", "sonnet"]

[defaults.network]

Network settings of the workspace VM and its network broker. [workspaces.<name>.network] takes the same fields; a set workspace field wins.

Field Type Default Description
ipv6 bool false Give the guest IPv6 (fd00:127::2/64) and let the broker allow IPv6 destinations by the IPv6 policy. When off, the guest has IPv4 only and the broker denies every IPv6 flow. Changing it needs jailoc restart; jailoc reload turns it off at the broker right away, except in a VM started by a jailoc release without this setting: that VM keeps IPv6 until jailoc restart.
dns string (host resolver) Routable numeric IPv4 resolver used for permitted DNS queries.
rules table[] [] Allow and deny egress rules ([[<scope>.network.rules]]). Combined across [defaults] and the workspace; deny always wins over allow.

[[<scope>.network.rules]]

Field Type Default Description
allow string[] (none) Hostnames, *.wildcard names, IP addresses, CIDRs, or "*" (public internet). Exactly one of allow or deny is required.
deny string[] (none) Hostnames, *.wildcard names, IP addresses, CIDRs, or "*". Exactly one of allow or deny is required.
proto string "tcp" "tcp", "udp", or "any".
ports (int | string)[] (all) Port numbers (1–65535) or "a-b" port ranges.
name string "" Label shown in jailoc traffic (rule=<name>) and validation errors.

Implicit rules and built-in broker behavior

In addition to [[defaults.network.rules]] and [[workspaces.<name>.network.rules]], the broker applies:

  • rule=agent (allow, proto = "tcp", ports = [443]): appended automatically for the workspace's selected coding agent (or the from agent of a custom agent). A matching deny rule still overrides it:
agent Implicit rule=agent allowlist (tcp/443)
opencode, opencode1, opencode2, omo api.anthropic.com, api.openai.com, auth.openai.com, chatgpt.com, generativelanguage.googleapis.com, oauth2.googleapis.com, api.githubcopilot.com, *.githubcopilot.com, openrouter.ai, opencode.ai, *.opencode.ai, models.dev
claude api.anthropic.com, claude.ai, console.anthropic.com, platform.claude.com
copilot api.github.com, github.com, api.githubcopilot.com, *.githubcopilot.com, copilot-proxy.githubusercontent.com
agy generativelanguage.googleapis.com, cloudcode-pa.googleapis.com, oauth2.googleapis.com, accounts.google.com
  • rule=legacy (proto = "any"): synthesized in memory when deprecated allowed_hosts, allowed_networks or dns_blocked_zones are set.
  • DoT and QUIC exclusion on "*": a wildcard rule (allow = ["*"]) without explicit ports excludes TCP port 853 (DNS over TLS) and UDP port 443 (QUIC/HTTP/3) so DNS stays on the gateway resolver and HTTPS clients fall back to TCP port 443 (where TLS SNI is inspected whenever hostname rules or hostname deny rules apply on that port) unless those ports are listed in ports.
  • Direct DNS block: TCP and UDP port 53 to any address other than the gateway (192.168.127.1) is denied (reason=dns-filtered).
  • Gateway services (192.168.127.1): the broker serves DHCP, DNS (port 53), SNTP clock sync (UDP port 123), SSH/GPG agent relays (ssh_auth_sock, gpg_agent), and configured host_ports on the gateway; all other connections to the gateway, guest subnet (192.168.127.0/24, fd00:127::/64), host interface addresses, and hard-denied ranges are always denied.
  • Default dev-deps rule in new configs: when jailoc creates a missing ~/.config/jailoc/config.toml, it writes an explicit [[defaults.network.rules]] entry named dev-deps into the file (GitHub, GitLab.com, npm, Yarn, Node.js, Bun, PyPI, Astral, Go proxy, crates.io, Rustup, Docker Hub, GHCR, Quay, GCR, MCR, Ubuntu/Debian/Alpine mirrors, and Nix caches). Removing or editing that block in config.toml removes or changes those rules.

Example

[defaults.network]
ipv6 = true
dns = "9.9.9.9"

[[defaults.network.rules]]
name = "dev-deps"
allow = ["github.com", "api.github.com", "*.githubusercontent.com", "registry.npmjs.org"]

[[defaults.network.rules]]
name = "no-paste"
deny = ["pastebin.com", "gist.github.com", "*.ngrok.io"]

[[workspaces.api.network.rules]]
name = "db"
allow = ["10.20.5.10"]
proto = "tcp"
ports = [5432]

[workspaces.<name>]

Each workspace is a TOML table under [workspaces], keyed by name. Names must match ^[a-z0-9-]+$.

Field Type Default Description
paths string[] [] Host directories shared into the VM over virtio-fs at their original absolute paths. The first path is the working directory; with no paths, the working directory is /home/agent.
backend string (inherit) "vm" or empty. "docker" is rejected as a jailoc v1 setting.
agent string (inherit) Coding agent of the workspace. See Coding agents.
agents table {} Settings per coding agent; a set field overrides [defaults.agents.<name>]. See agents.
image string (none) Pre-built image used directly, bypassing all build steps. Cannot be combined with dockerfile or build_context.
dockerfile string (none) Local path (/..., ~/...) or HTTP(S) URL to an overlay Dockerfile built on top of the base image. Build failure is fatal. Maximum file size for HTTP sources: 1 MiB.
build_context string (none) Build context of the overlay build. When empty, the Dockerfile's parent directory (local path) or a temporary directory (URL).
mounts string[] [] Directory shares for this workspace, merged with [defaults].mounts.
snapshots string[] [] Read-only snapshots, merged with [defaults].snapshots.
allowed_hosts string[] [] Merged with [defaults].allowed_hosts.
allowed_networks string[] [] Merged with [defaults].allowed_networks.
ports (int | string)[] [] Merged with [defaults].ports.
host_ports (int | string)[] [] Merged with [defaults].host_ports.
env string[] [] Overrides [defaults].env entries with the same key.
env_file string[] [] Loaded after [defaults].env_file.
secrets table {} Overrides default secrets with the same name.
network table {} Same fields as [defaults.network]; a set field wins.
ssh_auth_sock, gpg_agent, git_config, import_agent_config, cpu, memory, docker_disk, timezone, enable_docker, protect_git, isolated_build, ca_bundle (inherit) Same meaning as in [defaults]; the workspace value wins.

Example

[workspaces.my-project]
paths = ["~/projects/my-project", "~/shared/libs"]
allowed_hosts = ["api.example.com"]
snapshots = ["~/.npmrc:/home/agent/.npmrc"]
ports = [3000, "8080:80"]
dockerfile = "~/projects/my-project/overlay.Dockerfile"

Coding agents

Each workspace runs one coding agent. Sessions start it in unattended mode, because the VM is the security boundary.

agent Agent Package Session command Continue (-c) Resume (-s ID) Persistent home directories Environment
opencode2 OpenCode 2 @opencode/cli opencode --standalone --auto --continue --session ID ~/.local/share/opencode, ~/.local/state/opencode OPENCODE_DISABLE_AUTOUPDATE=1, OPENCODE_CLI_CONFIG_CONTENT
opencode1 OpenCode 1 opencode-ai opencode --continue --session ID ~/.local/share/opencode, ~/.local/state/opencode, ~/.omo OPENCODE_DISABLE_AUTOUPDATE=1, OPENCODE_TUI_CONFIG
claude Claude Code @anthropic-ai/claude-code claude --dangerously-skip-permissions --settings /etc/jailoc-tui-plugin/claude-settings.json --continue --resume ID ~/.claude CLAUDE_CONFIG_DIR=/home/agent/.claude, DISABLE_AUTOUPDATER=1
copilot GitHub Copilot CLI @github/copilot copilot --allow-all --continue --resume ID ~/.copilot COPILOT_AUTO_UPDATE=false
omo OmO omo-ai omo --permission-preset full-access --extension /etc/jailoc-tui-plugin/omo.js --continue --session ID ~/.omo PI_SKIP_VERSION_CHECK=1, OMO_RUNTIME=node
agy Antigravity CLI GitHub release google-antigravity/antigravity-cli agy --dangerously-skip-permissions --continue --conversation ID ~/.gemini AGY_CLI_DISABLE_AUTO_UPDATE=1

opencode is an alias of opencode2: it runs the same harness and shares its persistent home directories, while agents.opencode and agents.opencode2 are separate tables. OpenCode 2 runs with --standalone, so each session has its own server and environment instead of sharing a background service.

Every agent shows the workspace name; OPENCODE_TUI_CONFIG, OPENCODE_CLI_CONFIG_CONTENT, --settings and --extension load the jailoc indicator, and for copilot and agy jailoc adds a statusLine to ~/.copilot/settings.json and ~/.gemini/antigravity-cli/settings.json when it sets none (and storeTokenPlaintext = true in ~/.copilot/settings.json when unset, so Copilot stores logins without a system keyring). See Workspace indicator.

The package versions are pinned by the jailoc release; version in agents installs another one. The session command line is: the command above, the continue or resume flag, then args from agents.

jailoc installs the agent with npm install -g, or for agy downloads the release asset of the VM's architecture (x64 or arm64, glibc), checks its pinned SHA-256 and installs the binary as root into /usr/local/bin, in a separate layer on top of the base image, before the workspace dockerfile overlay, in every image it builds: the base image, a [base] dockerfile, and defaults.image with a workspace dockerfile. An image used directly (workspace image, or defaults.image without a workspace dockerfile) must contain the agent binary. Before a session starts, jailoc checks that the binary is on PATH in the VM.

Changing agent or the agent's profile of a running workspace takes effect after jailoc restart; sessions fail until then.

agents

Settings per agent live in [defaults.agents.<name>] and [workspaces.<ws>.agents.<name>], where <name> is a built-in agent or a custom agent. Only the settings of the selected agent apply.

Field Type Default Description
from string Custom agents only: the built-in agent it is derived from.
install bool true Install the agent in the images jailoc builds. With false, the image must provide the binary, for example through the workspace dockerfile.
version string (pinned) npm or release version installed instead of the version pinned by jailoc, e.g. "2.1.400". Must be an exact version; dist-tags such as latest are rejected. For agy, jailoc reads the checksums of another version's assets from the GitHub API.
args string[] [] Arguments appended to every session command line. A workspace list replaces the [defaults] list; args = [] clears it.
mounts string[] [] Mounts added while the agent is selected, same syntax and validation as mounts. An empty host (":/home/agent/.agents") removes a mount added by the agent or an earlier table. See Agent mounts.
profile false or string (unset) Where the agent's persistent home directories live. Unset: per workspace. false: per workspace, emptied on every VM start. A name: shared profile ~/.local/share/jailoc/profiles/<name>. See Agent profiles.
[workspaces.my-project]
paths = ["~/projects/my-project"]
agent = "copilot"

[workspaces.my-project.agents.copilot]
version = "1.0.110"
args = ["--model", "gpt-5"]

Custom agents

An agents table whose name is not a built-in agent defines a custom agent: the built-in agent named by from under another name, with its own settings. Select it with agent like a built-in one. It is installed, started and persisted like the agent it is derived from.

[defaults.agents.claude]
version = "2.1.400"

[defaults.agents.claude-opus]
from = "claude"
args = ["--model", "opus"]

[workspaces.my-project]
paths = ["~/projects/my-project"]
agent = "claude-opus"

A custom agent defined in [defaults] can be selected and configured in any workspace; one defined in a workspace only there. Each field except mounts and profile takes the first value set in: the workspace's table of the agent, the defaults' table of the agent, then, for a custom agent, the workspace's and the defaults' table of the agent it is derived from. In the example, claude-opus installs Claude Code 2.1.400.

Agent mounts

The mounts of the selected agent are merged by guest path; a later entry replaces an earlier one with the same guest path, an empty host removes it. The order is:

  1. the built-in mounts of the agent's harness, including ~/.agents:/home/agent/.agents:ro
  2. mounts of the built-in agent's table in [defaults], then in the workspace
  3. for a custom agent, mounts of its own table in [defaults], then in the workspace
  4. [defaults].mounts, then the workspace's mounts
[defaults.agents.opencode-legacy]
from = "opencode"
mounts = ["~/.config/opencode-legacy:/home/agent/.config/opencode"]

A mount at the destination of an import_agent_config copy replaces the copy.

Agent profiles

profile selects where the agent's persistent home directories (see Coding agents) are stored. It is read from the selected agent's own tables: the workspace's, then the defaults'. A custom agent does not inherit profile from the agent it is derived from.

Value Storage Lifetime
unset ~/.cache/jailoc-vm/workspaces/<workspace>/guest/home/<dir> until jailoc down --purge
false per workspace emptied on every VM start
"<name>" ~/.local/share/jailoc/profiles/<name>/<dir> until deleted by hand; jailoc down --purge keeps it

<dir> is the name of the harness's state directory (opencode2-data, opencode2-state for OpenCode 2, opencode-data, opencode-state for OpenCode 1, plus opencode1-omo for the OmO plugin's ~/.omo, claude, copilot, omo, antigravity), so one profile can hold the state of several harnesses, and two agents of the same harness share it when they name the same profile. ~/.cache stays per workspace. A mount over a persistent home directory replaces it.

A named profile is used by one running workspace at a time: jailoc up and jailoc restart fail while another workspace using the profile is running.

Warning

Every workspace using a named profile can change what the harness executes in the others (hooks, plugins, MCP servers, instructions stored in its state directory), read the login and resume the sessions stored there. Share a profile only between workspaces of the same trust level. See Threat Model.

[workspaces.old-project.agents.opencode]
profile = "opencode-legacy"

Validation Rules

jailoc v1 settings

These settings fail config load with <setting> is a jailoc v1 setting; run `jailoc migrate` to update the config:

  • backend = "docker" in [defaults] or a workspace
  • top-level mode and password_mode
  • expose_port in [defaults] or a workspace

Any backend value other than "vm" and empty is rejected. See Migrate from jailoc v1.

Moved network settings

filtered_dns, dns_upstream and dns_blocked_zones directly in [defaults] or a workspace fail config load with [defaults]: filtered_dns moved to the [defaults.network] table; run `jailoc migrate` to update the config (or the workspace's network table). jailoc migrate moves them.

paths

~ is expanded. An empty list is allowed; empty entries are rejected. Paths equal to or under these prefixes are rejected: /home/agent, /usr, /etc, /var, /bin, /sbin, /lib, /lib64, /opt, /root, /proc, /sys, /dev, /run, /tmp, /certs.

Several workspaces may share a path, for example to run different agents in the same project. When the current directory matches several workspaces at the same depth, jailoc fails and asks for -w.

Each path is a separate virtio-fs share. A VM boots with at most 11 shares on linux/amd64 hosts and 120 on arm64 hosts (macOS and Linux), one fewer with enable_docker. Paths, mounts with a host directory and the state directories of a named agent profile count towards the limit; jailoc up and jailoc reload fail above it. List a common parent directory instead of many sibling directories.

mounts

Format: host:container[:mode], mode ro or rw (default rw). Mounts are virtio-fs directory shares.

  • host: absolute or ~ path. Must be a directory; a file mount fails at jailoc up. Use snapshots for files. An empty host (":/home/agent/.agents") removes an inherited mount with that destination.
  • container: absolute, or ~/..., where ~ stands for the guest home /home/agent. Rejected when equal to or under /, /usr, /etc, /var, /bin, /sbin, /lib, /lib64, /opt, /root, /proc, /sys, /dev, /run, /tmp, /certs. Destinations under /home/agent/... are allowed.

Host paths equal to or under these entries are rejected: /, /boot, /dev, /etc, /private, /proc, /sys, /run, /var, ~/.ssh, ~/.gnupg, ~/.aws, ~/.azure, ~/.config/gcloud, ~/.config/gh, ~/.docker, ~/.kube, ~/.netrc, ~/.config/jailoc, ~/.cache/jailoc-vm. Parents of the entries under ~ are rejected too, so ~, ~/.config and ~/.cache cannot be shared whole. Entries under ~ are also compared after resolving symlinks, so a symlink to ~/.ssh, or a dotfiles directory that ~/.ssh links into, is rejected as well. The error explains why the matched entry is protected; for credential directories it shows SSH or GPG agent forwarding or a file secret holding the one file a tool needs, and for ~/.config/jailoc and ~/.cache/jailoc-vm it suggests keeping shared directories outside them or using an agent profile. See Pass a credential file.

snapshots

Format: host:container, optionally :ro; :rw is rejected because snapshots are read-only. Host and destination rules are the same as for mounts, except that the host may be a file or a directory. An empty host removes an inherited snapshot.

jailoc copies the sources when the VM starts and again on every jailoc reload, which also applies added and removed entries. At jailoc up and jailoc reload, the destination must not equal, contain or lie inside a shared path (workspace paths, mounts destinations, the persistent home directories), and the source must exist. A symlink given as the source root is resolved; symlinks inside a copied directory are copied unchanged.

git_config

When true, jailoc adds read-only snapshots of the host Git configuration at jailoc up and jailoc reload:

Host Guest
~/.gitconfig /home/agent/.gitconfig
$XDG_CONFIG_HOME/git/config /home/agent/.config/git/config
$XDG_CONFIG_HOME/git/ignore /home/agent/.config/git/ignore
$XDG_CONFIG_HOME/git/attributes /home/agent/.config/git/attributes

$XDG_CONFIG_HOME defaults to ~/.config. Missing files are skipped. Other files in the directory are not copied. The Git credential stores ~/.git-credentials, $XDG_CONFIG_HOME/git/credentials and ~/.config/git/credentials are never copied, whichever path or link names them.

Files referenced by the copied configuration files are copied to the same path relative to /home/agent when they lie under the host home directory: include.path and includeIf.<condition>.path (followed recursively, relative paths resolved against the including file; the included file must stay under the home directory or $XDG_CONFIG_HOME), core.excludesFile, core.attributesFile, commit.template, gpg.ssh.allowedSignersFile, and user.signingKey when its value is a path. The home directory check uses the path as written, so a symlink under the home directory may point elsewhere, such as a dotfiles checkout or the Nix store. The credential checks apply to both the path as written and the file it resolves to, including hard links to files in ~/.ssh, ~/.gnupg and ~/.aws and in the directories symlinks there point to, and the copy holds the content that was checked. A referenced file that is missing, outside the home directory, not a regular file of at most 1 MiB, a Git credential store, in ~/.ssh, ~/.gnupg or ~/.aws, or whose path in the VM is already taken by a different file is not copied and produces a warning. gpg.ssh.allowedSignersFile and user.signingKey are exceptions to the directory rule but copy public data only: the user.signingKey copy holds the first SSH public key in the file, and the gpg.ssh.allowedSignersFile copy holds its signer lines (principals, the cert-authority, namespaces, valid-after and valid-before options, and the public key) without comments. A user.signingKey file without an SSH public key, or an allowed signers file with any other line, is not copied and produces a warning. At most 256 configuration files are read.

Configuration files are copied with two rewrites: values of the path variables above that lie under the host home directory become ~/..., and includeIf "gitdir:~/..." and includeIf "gitdir/i:~/..." conditions use the host home directory, which workspace paths keep inside the VM.

Each copy is a default snapshot: a snapshots entry with the same destination replaces it, an entry with an empty host removes it, and a directory snapshot that contains its destination replaces it.

import_agent_config

When true, jailoc adds read-only snapshots of the selected agent's host configuration at jailoc up and jailoc reload:

Agent Host, copied to the same path under /home/agent
opencode, opencode2 ~/.config/opencode, without node_modules
opencode1 ~/.config/opencode, without node_modules; ~/.omo/omo.jsonc, ~/.omo/omo.json
claude ~/.claude/CLAUDE.md, ~/.claude/RTK.md, ~/.claude/settings.json, ~/.claude/agents, ~/.claude/commands, ~/.claude/hooks, ~/.claude/skills, ~/.claude/output-styles
copilot ~/.copilot/copilot-instructions.md, ~/.copilot/agents, ~/.copilot/hooks, ~/.copilot/skills, ~/.copilot/mcp-config.json
agy ~/.gemini/GEMINI.md
omo ~/.omo/omo.jsonc, ~/.omo/omo.json

Missing sources are skipped. Logins, sessions and the agents' own config.json and settings.json of copilot stay in the persistent state directory. The OpenCode copy gets the .gitignore OpenCode would write (unless the host directory has one), and OpenCode doesn't install the dependencies of local plugins in the read-only directory: plugins listed in plugin still install into ~/.cache, but a local plugin that imports a package needs it installed in the image.

The copies inside a persistent state directory (~/.claude, ~/.copilot, ~/.omo, ~/.gemini) hide what is there in the VM: a skill the agent installs into ~/.claude/skills is not visible while that directory is copied. When the snapshot is dropped, the empty mount points it left in the state directory are removed at the next start.

Each copy is a default snapshot like the Git configuration: a snapshots entry with the same destination replaces it, an entry with an empty host removes it (snapshots = [":/home/agent/.claude/settings.json"]), and a directory snapshot that contains its destination replaces it. A copy whose destination equals, contains or lies inside a shared path, such as a mount of ~/.claude or a named profile state directory, is skipped.

ports

Each entry is a port number (3000: guest 3000 on host 3000) or "HOST:GUEST" ("8080:80": guest 80 on host 8080), with ports 1 to 65535. A host port may appear only once per list. A workspace entry replaces a [defaults] entry with the same host port. Ports are published on host 127.0.0.1 only.

host_ports

Each entry makes a host service reachable from the guest:

Entry Guest port Host target
9222 9222 127.0.0.1:9222
"8931:3845" 8931 127.0.0.1:3845
"7100:/tmp/mcp.sock" 7100 Unix socket /tmp/mcp.sock (absolute or ~/)

Ports are 1 to 65535. A guest port may appear only once per list; a workspace entry replaces a [defaults] entry with the same guest port. Guest ports 22, 53, 2375, 7022 and 7023 are reserved.

Inside the guest each port is reachable on the gateway 192.168.127.1, on the names host.jailoc.internal and host.docker.internal, and on 127.0.0.1 and [::1] when nothing in the guest already listens on that port. TCP targets are always host loopback. A Unix socket that does not exist yet produces a warning; connections fail until it exists.

jailoc warns when an entry targets port 9222 (Chrome DevTools Protocol) or a container runtime socket (docker.sock, containerd.sock, podman.sock, buildkitd.sock).

Warning

Host services reached through host_ports bypass the network policy. Anything in the guest gets the same access to them as a local process on the host.

allowed_hosts and allowed_networks

allowed_networks entries must parse with Go's net.ParseCIDR; IPv4 and IPv6 CIDRs are accepted. allowed_hosts entries are resolved on the host for A and AAAA records, and each address is allowed as a /32 or /128. Some IPv6 ranges stay denied regardless of these lists; see Network isolation.

Filtered DNS

When network.filtered_dns = true, an effective dns_upstream and at least one effective dns_blocked_zones entry are required. dns_upstream must be a numeric IPv4 address other than unspecified, loopback, link-local or multicast. Each zone must be a DNS name of labels with letters, digits or internal hyphens (63 characters per label, 253 total); a trailing dot is accepted and "." is rejected. Queries for names in blocked zones get NXDOMAIN, matched case-insensitively including subdomains, and so do answers that name a blocked zone anywhere, for example through a CNAME. Queries with more or fewer than one question are refused. Direct DNS to port 53 of other servers is blocked.

With filtering on, an effective allowed_hosts entry equal to or under an effective blocked zone is rejected at config load.

ca_bundle

  • Omitted or true: automatic discovery. The non-empty SSL_CERT_FILE environment variable is used first, then NIX_SSL_CERT_FILE. If the selected variable points at an invalid source, jailoc up fails without trying the next one. If neither is set, no bundle is forwarded.
  • false: no bundle.
  • Non-empty string: custom bundle path, absolute or ~. Symlinks are resolved; the target must be a readable regular file with at least one CERTIFICATE PEM block and no private-key PEM block.
  • Empty string or any other TOML type: rejected at config load.

The bundle is added to the VM's system trust store, which its Docker daemon also uses, and to the builder VM's Docker daemon. Sessions get NODE_USE_SYSTEM_CA=1 when a bundle is forwarded.

enable_docker

With enable_docker = true, jailoc starts dockerd inside the workspace VM; an image without dockerd fails jailoc up. enable_docker = true together with protect_git = true is rejected, because Docker access is root access inside the VM; protect_git alone turns Docker off. In the images jailoc builds, Docker is installed only when enable_docker is on; see Docker layer.

cpu, memory and docker_disk

cpu must be a finite number greater than 0. memory and docker_disk must match ^[1-9][0-9]*[kmgKMG]?$ (for example 512m, 4g). At jailoc up, memory must be at least 512m and docker_disk at least 1g; docker_disk is rounded up to whole MiB. When the existing data disk is larger than docker_disk, jailoc up fails instead of shrinking it.

timezone

timezone must match ^[A-Za-z0-9_+-]+(/[A-Za-z0-9_+-]+)*$; absolute paths and POSIX TZ rules are rejected. Without it, the VM takes the host's zone from TZ when set, otherwise from /etc/localtime. jailoc-guest points /etc/localtime at the zone in the image's /usr/share/zoneinfo, or copies the host's data for that zone when the image lacks it, and writes the name to /etc/timezone. A zone that neither the image nor the host has leaves the image's zone in place. jailoc reload applies a changed timezone to a running VM.

image, dockerfile, build_context

image cannot be combined with dockerfile or build_context, and must not be whitespace-only. dockerfile accepts absolute paths, ~ paths and http/https URLs with a non-empty host (http:///path is rejected). Relative paths and other schemes are rejected.

agent and agents

agent must be opencode, opencode1, opencode2, claude, copilot, omo, agy or a custom agent defined in the workspace's or the defaults' agents. A key of agents that is not a built-in agent is a custom agent: its name must match ^[a-z0-9-]+$ and from, set in that table or in the defaults' table of the same name, must be a built-in agent (custom agents cannot be derived from custom agents). from is rejected on built-in agents. version must be an exact version (1.2.3, optionally with a -prerelease and +build suffix). mounts follows the rules of mounts. profile must be false or a name matching ^[a-z0-9-]+$; true is rejected.

env

Each entry must be KEY=VALUE with a non-empty key. Reserved keys, which jailoc sets itself: JAILOC, JAILOC_WORKSPACE, NODE_USE_SYSTEM_CA, SSH_AUTH_SOCK, and the agent variables listed in Coding agents for every agent: OPENCODE_DISABLE_AUTOUPDATE, OPENCODE_TUI_CONFIG, OPENCODE_CLI_CONFIG_CONTENT, CLAUDE_CONFIG_DIR, DISABLE_AUTOUPDATER, COPILOT_AUTO_UPDATE, PI_SKIP_VERSION_CHECK, OMO_RUNTIME, AGY_CLI_DISABLE_AUTO_UPDATE.

env_file

Paths must be absolute or start with ~, and the files must exist at config load time. Format: KEY=VALUE lines, # comments, empty lines ignored, quoted values allowed, inline comments stripped. Values are literal; no host variable expansion. Entries are checked against the reserved keys.

secrets

Secrets live under [defaults.secrets.env.<NAME>], [defaults.secrets.file.<NAME>], [workspaces.<ws>.secrets.env.<NAME>] or [workspaces.<ws>.secrets.file.<NAME>]. The sub-table sets the destination, the field sets the source.

Field Type Description
from_env string Host environment variable to read. Must match ^[A-Za-z_][A-Za-z0-9_]*$.
from_file string Host file to read. Absolute or ~ path.
from_command string Command run on the host with /bin/sh -c; its standard output is the value. Must not be blank.
ttl string Duration (time.ParseDuration, e.g. "15m", "1h") for which from_command output is cached in the host system keychain per workspace. Only valid with from_command; must be positive.

Exactly one of from_env, from_file and from_command must be set.

Destination <NAME> rule In the guest
secrets.env.<NAME> ^[A-Za-z_][A-Za-z0-9_]*$; not HOME, PATH or a reserved env key exported as <NAME> in session environments
secrets.file.<NAME> ^[a-zA-Z0-9_-]+$ /run/secrets/<NAME>

A name may not appear in both secrets.env and secrets.file of the same scope.

At jailoc up, jailoc reload and jailoc add, jailoc reads every source on the host: a from_env variable must be set and non-empty, a from_file path must be a regular file, and a from_command must exit with status 0, print something and finish within two minutes. Commands run with the terminal attached and JAILOC_WORKSPACE set to the workspace name. When ttl is set on a from_command secret, jailoc caches the command output in the host system keychain (macOS login keychain or Linux kernel user keyring) for that duration, keyed by configuration file, workspace, secret destination, secret name and command; jailoc reload --purge-cache evicts the cached entries and runs the commands again, and jailoc down --purge deletes them. Both fail when the keychain refuses to delete an entry and keep it recorded, so the next purge retries it. Env secret values lose trailing \r and \n characters and must not contain line breaks or NUL. Values travel to the guest over SSH; file secrets are written to /run/secrets (tmpfs, mode 0400, owned by the agent) and env secrets go to the session environment only. jailoc never writes them to plain files on host disk. jailoc reload re-reads and re-pushes secrets and removes files of secrets no longer configured; running sessions keep their old environment. See the secrets how-to.


Merge Semantics

Environment variables merge in this order, later wins per key:

  1. [defaults].env
  2. [defaults].env_file (in list order)
  3. workspace env_file (in list order)
  4. workspace env

allowed_hosts and allowed_networks are unions of the defaults and workspace lists, deduplicated in order.

mounts merges three layers keyed by destination: built-in default mounts of the workspace's agent, [defaults].mounts, workspace mounts. The last layer to set a destination wins; an empty host removes it. Built-in default mounts:

Agent Destination Host source Mode
all /home/agent/.agents ~/.agents ro

The agent's configuration is copied instead of mounted; see import_agent_config. The OmO plugin (oh-my-openagent) of opencode1 writes its configuration, state and lock files to ~/.omo, which is a persistent home directory of the workspace. To share the host's ~/.omo instead, mount it: [defaults.agents.opencode1] mounts = ["~/.omo:/home/agent/.omo:rw"].

snapshots merges [defaults].snapshots and workspace snapshots keyed by destination. git_config, import_agent_config and ssh_auth_sock add lowest-priority snapshots (the Git configuration files, the agent configuration, /home/agent/.ssh/known_hosts), which a snapshots entry for the same destination replaces or, with an empty host, removes.

ports merges by host port; the workspace entry wins. host_ports merges by guest port; the workspace entry wins.

secrets merges by name. A workspace secret replaces a default secret with the same name entirely, also across env and file. An inherited default secret can't be unset in a workspace.

Scalar settings (agent, ssh_auth_sock, gpg_agent, git_config, import_agent_config, cpu, memory, docker_disk, timezone, enable_docker, protect_git, isolated_build, ca_bundle, and network.ipv6, network.filtered_dns) take the workspace value when set, otherwise [defaults], otherwise the built-in default. The same applies to each field of agents.<name>; a custom agent then falls back to the tables of the agent it is derived from (see Custom agents). agents.<name>.mounts merges instead (see Agent mounts), and profile is not inherited from the derived-from agent. An empty workspace network.dns_upstream and an omitted network.dns_blocked_zones inherit the defaults; a set dns_blocked_zones replaces the default list.


Persistent state

~/.cache/jailoc-vm/workspaces/<workspace>/guest/home keeps the agent's persistent home directories (see Coding agents) and ~/.cache across restarts. jailoc down --purge deletes it. An agent profile moves the agent's directories to a shared profile or makes them ephemeral.


See the workspace configuration how-to for step-by-step setup.