Skip to content

How to configure workspaces

This guide shows how to define and tune workspaces in jailoc's configuration file. For every field, its type and its validation rules, see the Configuration reference.

Separate guides cover network access (allowed_hosts, allowed_networks), custom images (image, dockerfile, build_context), SSH and Git passthrough (ssh_auth_sock, gpg_agent) and coding agents (agent, agents).

Find the config file

jailoc stores its configuration at:

~/.config/jailoc/config.toml

The file is created on first run with a default workspace already in place. Edit it with any text editor.

To use another file, pass --config or set JAILOC_CONFIG:

jailoc --config ~/work/jailoc.toml up
export JAILOC_CONFIG=~/work/jailoc.toml

The file must exist. Give its workspaces names that no other configuration file uses: VMs are identified by workspace name alone. See File location.

Select a workspace

Commands use the workspace whose paths contain the current directory, the most specific one if several do, and fall back to default. Given these two workspaces:

[workspaces.broad]
paths = ["/home/you/projects"]

[workspaces.specific]
paths = ["/home/you/projects/api"]

Running jailoc up from /home/you/projects/api/src selects specific. Running it from /home/you/projects/other selects broad. To pick a workspace explicitly, pass its name:

jailoc up api
jailoc -w api sessions

See Workspace resolution for the exact order.

Define a workspace

Each workspace is a [workspaces.<name>] section. Most workspaces set paths; the first path is the working directory. A workspace without paths starts in /home/agent:

[workspaces.default]
paths = ["/home/you/projects/myproject"]

Each path is shared into the VM read-write at its original absolute path, so /home/you/projects/myproject has the same path inside the guest. Files the agent creates there belong to your host user.

Note

~ expands to your home directory. Paths under system directories (/usr, /etc, /var, /home/agent, /tmp and similar) are rejected.

Give a workspace several paths

List more than one directory when the agent needs several repositories at once:

[workspaces.api]
paths = [
  "/home/you/projects/api",
  "/home/you/projects/shared-libs",
]

Each workspace gets its own VM. Define as many [workspaces.<name>] sections as you need.

Set environment variables

Pass variables to agent sessions with env, or load them from files with env_file. Env files must exist at config load time and use .env format (KEY=VALUE, # comments, quoted values):

[defaults]
env = ["GOPRIVATE=*.example.com"]
env_file = ["~/.config/jailoc/shared.env"]

[workspaces.api]
paths = ["/home/you/projects/api"]
env = ["LOG_LEVEL=debug"]

env entries override env_file entries with the same key.

Note

Keys jailoc sets itself are rejected: JAILOC, JAILOC_WORKSPACE, NODE_USE_SYSTEM_CA, SSH_AUTH_SOCK and the agent variables listed in Coding agents.

For tokens and passwords, use secrets instead of env.

Share extra directories with mounts

mounts are live, two-way shares. Each entry has the form host:guest[:mode], where mode is ro or rw (the default):

[defaults]
mounts = ["~/.config/ai-instructions:/home/agent/.config/ai-instructions:ro"]

Per-workspace mounts override [defaults] entries with the same guest path. To drop a default mount for one workspace, give it an empty host side:

[workspaces.my-project]
paths = ["/home/you/projects/my-project"]
mounts = [":/home/agent/.config/ai-instructions"]

Warning

Mounts share directories only. A mount whose host side is a file fails jailoc up with shares directories only; use snapshots. Use snapshots for single files.

Note

Dangerous host paths 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, and directories that contain one of the home entries, such as ~, ~/.config or ~/.cache. To give the agent one credential from such a directory, use a file secret; the error message shows the entry to add. Guest destinations under /home/agent/... are allowed; other system directories are rejected. On the guest side, ~ stands for /home/agent: ~/.config/ai-instructions and /home/agent/.config/ai-instructions are the same destination.

Snapshot files into the workspace

For single files, or when the workspace should get a frozen copy instead of the host original, use snapshots:

[workspaces.myproject]
paths = ["~/src/myproject"]
snapshots = [
  "~/.npmrc:/home/agent/.npmrc",
  "~/.config/git/ignore:/home/agent/.config/git/ignore",
]

Each snapshot is copied when the VM starts and is read-only inside the guest, even for root. Nothing the workspace does reaches the host original. A symlink given as the snapshot source is resolved; symlinks inside a snapshotted directory are copied as symlinks. A snapshot destination can't overlap a shared path, so it can't point into a workspace path or a mounts destination. To pick up host edits, or added and removed entries, run jailoc reload. Credential directories such as ~/.config/gh can't be snapshotted; for the GitHub CLI, pass a token as an env secret instead (see Pass a credential file).

Publish ports

To reach a service in the VM from the host, publish its port:

[workspaces.myproject]
ports = [3000, "8080:80"]

3000 makes guest port 3000 available on host 127.0.0.1:3000. "8080:80" makes guest port 80 available on host 127.0.0.1:8080. The service must listen on 0.0.0.0 (or the guest address 192.168.127.2), not on 127.0.0.1. Containers started with docker run -p inside the guest are reachable the same way.

Reach host services

The network broker blocks the guest from the host. To let the agent use a service that listens on the host, such as a local MCP server or a browser started with --remote-debugging-port, list it in host_ports:

[workspaces.myproject]
host_ports = [9222, "8931:3845", "7100:~/.cache/mcp.sock"]

9222 relays guest port 9222 to host 127.0.0.1:9222. "8931:3845" relays guest port 8931 to host 127.0.0.1:3845. "7100:~/.cache/mcp.sock" relays guest port 7100 to a host Unix socket. In the guest, connect to any of:

  • localhost:9222 (unless a guest process already listens on that port)
  • host.jailoc.internal:9222 or host.docker.internal:9222
  • 192.168.127.1:9222

For example, attach Playwright MCP to a host Chrome:

# on the host, with a dedicated profile
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.cache/jailoc-chrome"
# in the guest
npx @playwright/mcp --cdp-endpoint http://localhost:9222

The host service must listen on 127.0.0.1 (or the socket path). Remove an entry and run jailoc reload to cut access; open connections are closed.

Warning

Each entry gives the agent the same access to that service as a local process on the host. A browser debugging port controls the browser with all its logins, and a container runtime socket is root on the host. jailoc warns about both.

Protect Git hooks and configuration

Workspace paths are shared read-write, including .git. Git on the host runs hooks and commands named in .git/config (such as core.fsmonitor), so an agent that writes there can run code on your host. Prevent it with:

[workspaces.myproject]
protect_git = true

At every jailoc up, jailoc finds the repositories, including bare ones, under the workspace paths and read-write mounts, including symlinked workspace roots. Inside the VM, it makes .git/hooks, .git/config, config.worktree, linked worktree commondir pointers, every core.hooksPath directory and files named by include.path or includeIf read-only, whether or not the includeIf condition matches, including metadata of submodules, worktrees and separate git directories reached through .git files. A relative core.hooksPath is protected both below the working tree and below the git directory. core.hooksPath values from the host's system and global Git configuration count too, so a host-wide hooks directory inside a share is protected. A repository visible at several paths in the VM is protected at each of them; one hidden under a read-only mount is skipped. A missing .git/config or config.worktree is created empty so that it can be protected. jailoc up fails if the path to one of these files runs through a symbolic link inside a share, because the link itself could be redirected, and also if a protected file or hook has another hard link, a configuration file is not a regular file or is larger than 1 MiB, a configuration includes more than 256 files, a hook links to a missing file, a hooks directory has more than 1024 entries, or a commondir points at another linked git directory. These git directories and their parents cannot be renamed or removed. sudo and Docker are disabled in that workspace, and enable_docker = true together with protect_git = true is rejected.

Warning

Only repositories that exist at jailoc up are covered. Repositories the agent creates or clones later aren't protected until the next restart, and repositories inside node_modules, .venv, venv, __pycache__, .tox, .terraform, .next, .gradle and .cache aren't searched. Files that tools run from the working tree, such as Makefile or package.json scripts, aren't covered either. The protection is enforced inside the guest; it holds against the agent, not against an exploit of the guest kernel.

Use Docker inside the VM

Docker is off by default. Turn it on for a workspace, or for all of them in [defaults]:

[workspaces.my-project]
enable_docker = true

The guest then runs its own Docker daemon. In the images it builds, jailoc adds Docker CE with the buildx and compose plugins; changing enable_docker takes effect after jailoc restart, which rebuilds the image. Images, containers and volumes live on a data disk that survives jailoc down; jailoc down --purge deletes it. The disk is 20 GiB unless you set docker_disk:

[workspaces.myproject]
enable_docker = true
docker_disk = "100g"

The disk is a sparse file, so it takes host space only for data written to it. Raising docker_disk grows the disk and its file system after jailoc restart; the disk can't shrink, so to make it smaller, delete it with jailoc down --purge. With isolated_build, the workspace's builder VM gets a data disk of the same size.

An image used directly (image) must contain dockerd; otherwise enable_docker = true fails jailoc up.

Set CPU and memory

[defaults]
cpu = 2.0
memory = "4g"

[workspaces.heavy-agent]
cpu = 8.0
memory = "16g"

Workspace values override defaults. Without either, the VM gets 2 vCPUs and 4g. cpu is rounded up to whole vCPUs. memory is a positive integer optionally followed by k, m or g, and must be at least 512m. The limits apply to the whole VM, including its Docker daemon.

Inside the VM, jailoc swaps to compressed memory (zram, half of memory) and trims part of the file cache every minute; freed pages are reported to the host through the balloon device. See Resource use.

Apply changes to a running workspace

After editing the config, apply it without restarting:

jailoc reload myproject

Network rules, DNS filtering, ports, host_ports, ssh_auth_sock, gpg_agent, ca_bundle, snapshots, env, env_file and secrets apply live. Running sessions keep their old environment. Other changes, such as paths, mounts, cpu, memory, docker_disk, enable_docker, protect_git and the image, need a new VM. jailoc reload lists them; apply them with jailoc restart, which ends running sessions.