Skip to content

FAQ

Short answers to the questions the rest of the documentation answers at length. Every entry links to the page that covers it properly — this page is an index into the docs, not a replacement for them.

Is JailBee for me?

What does JailBee actually give me?

One unprivileged Incus system container per git branch — its own init, Docker daemon, IP, services and database — so several branches run in parallel on one Linux host without port or Compose-name collisions. The container is also a git remote, so commits move between host and container locally.

Who JailBee is for, Architecture

What do I need on my machine?

A Linux host (Ubuntu 26.04+ recommended), Incus 6.0.5-8 or newer, and a way to install a Python CLI (uv or pipx). Host-installed Chrome and JetBrains Toolbox are needed only for the GUI passthrough features.

Prerequisites

Can I use it from a Mac?

Only by running JailBee inside a Linux VM (Colima/Lima) with the repo shared from macOS — the Incus daemon is Linux-only, and a native macOS client cannot make the Linux daemon bind-mount macOS paths. That path is experimental and not yet verified on real Apple hardware, and jailbee ide, jailbee chrome and GPG signing are unavailable there.

Running JailBee on macOS, Limitations

What does it cost me compared to git worktree + docker compose -p?

Real host setup (an afternoon, once), a ~10–15 min golden-image build per repo stack, disk per container, Linux-only, and a shared kernel rather than a hypervisor boundary. If your stack is one process and one database, worktrees plus Compose are free and already installed.

What JailBee costs you

Installation and first run

How do I install the CLI?

uv tool install jailbee            # or: pipx install jailbee
uv tool install 'jailbee[gui]'     # with the optional Qt dashboard

This installs jailbee and the short alias jb. JailBee is an ordinary PyPI package — a virtualenv you manage yourself works too; only a bare pip install into Ubuntu's system Python is refused (PEP 668).

Install JailBee

How do I know when a new release is out?

JailBee tells you. When it has seen a newer release on PyPI, jailbee ls / new / shell print one line on stderr naming the version and the upgrade command for your install (uv tool upgrade jailbee, pipx upgrade jailbee, …). jailbee dismiss update silences one release; update_check: false in ~/.config/jailbee/global.yaml — or JAILBEE_NO_UPDATE_CHECK=1 for one command — turns the whole thing off.

The check costs your commands nothing and reports nothing about you: the version is fetched by a detached background process at most once a day, in one anonymous GET of https://pypi.org/pypi/jailbee/json, and the commands themselves only read the answer it left behind. An install running from a checkout is never advised to upgrade.

update_check

What is the minimum host setup?

Four one-time steps: install and initialise Incus, add the two /etc/subuid//etc/subgid delegation lines, install the CLI, and check jailbee version. Everything past that in the install page is conditional — apply it only if jailbee doctor or the symptom says so.

Quick install

Why does it need an extra /etc/subuid line?

JailBee mounts a few host files (~/.gitconfig, ~/.gnupg, the GPG agent socket) into every container as your real host UID via raw.idmap, while the container otherwise runs unprivileged. newuidmap refuses to install that carve-out unless it is delegated. The security impact is effectively none: it authorises root to delegate exactly one UID — your own.

Without the line, containers are created and stay STOPPED, with the reason only in incus info --show-log <name>. jailbee doctor's uid delegation check reads both files and names the missing line.

Why the UID mapping is needed

What do I run the first time in a repo?

jailbee config init      # write .jailbee/config.yaml
jailbee doctor           # sanity-check host + config
jailbee init             # profiles, ACL, jailbee-loose bridge, shared dirs
jailbee registry up      # Docker users only — host-level Docker registry mirror
jailbee base build       # golden image, ~10–15 min, one time
jailbee new feat/x       # first container

Initialize and build, Setting up JailBee in your own project

When do I have to rebuild the golden image?

After changing anything the image bakes in — golden.stacks, golden.extra_apt_packages, golden.enable_snippets, or a custom provision_script. Run jailbee base build again; existing containers keep the old image until they are recreated. Old dated archives are cleaned up with jailbee base prune.

Stack runtimes and extra apt packages

What is the difference between jailbee init and jailbee apply?

init is first-time setup — it creates the Incus profiles, the ACL and the shared directories. apply re-applies the current config to what already exists (profiles, ACL, /etc/hosts, dockerd proxy) and offers to restart containers when profiles changed. It never restarts anything without asking: if the registry-mirror CA or proxy drop-in changed, it offers a dockerd restart separately, because that stops the Docker containers running inside. Run apply after editing config; apply --no-restart pushes an egress change live without a restart.

Commands

Everyday container work

How do I create a container for a branch?

jailbee new <name> [<base>]. <name> is the environment name and the branch inside the container — Incus names can't contain /, so feat/my-feature becomes the container feat-my-feature. <base> sets the base branch: it is forked when <name> is new, and used purely as the comparison anchor when <name> already exists. jailbee new --current takes the host's checked-out branch.

Choosing the starting point for jailbee new

jailbee new blocks for minutes — can I get my shell back?

Pass --background (-b). jailbee ls then shows a JOB column with the live phase (creatingcloningautostart). A failed job leaves the container intact for inspection; clear the record with jailbee job clear once you've fixed it, and read the worker log with jailbee job log. Set new.background: true to make it the default.

jailbee start and jailbee restart take the same flag — there the wait is the on_start autostart run, not the boot — with boot.background: true as the config default for both. A failed boot record needs no acknowledging: the next jailbee start/jailbee restart that completes clears it, since that boot supersedes the one that failed. A failed jailbee new record does not clear itself — the container's setup never finished, and a reboot doesn't finish it.

Background creation

How do I get into a container?

jailbee shell <name> for an interactive shell (it lands in the in-container clone), jailbee tmux <name> to attach to the autostart tmux session, and jailbee exec <name> -- <cmd> to run one command as the dev user.

Commands

How do I see what containers exist?

jailbee ls for this repo, jailbee ls --all for every jailbee-managed repo, jailbee dashboard for a live TUI across all repos, and jailbee gui for the Qt version. The dashboards also act: shell, tmux, IDE, PR, diff, update from base.

Commands

Clone mode or mount mode?

Default is clone mode: the container gets its own git clone --shared of the repo, isolated from the host tree, and the git bridge moves commits. --mount bind-mounts the host working tree instead — right when the host directory isn't a git repo at all, or when you edit on the host and run in the container. The bridge commands refuse on mount-mode containers.

Mount mode vs clone mode

How do I take a snapshot before something risky?

jailbee snapshot create <name> before-migration
jailbee snapshot restore <name> before-migration

Worth doing before a long unattended agent run.

Commands, Running an agent without prompts

How do I tear a container down without losing work?

jailbee destroy <name> (or --all for the repo). Before the usual confirmation JailBee assesses what would be lost — dirty tree, changed submodule, commits held nowhere else — and asks a second time, defaulting to No, if anything is at risk. --force skips both prompts and the assessment. jailbee prune cleans up stale containers interactively.

Commands

Moving code between host and container

How do I get the container's commits onto the host?

jailbee git checkout <name>   # fetch + fast-forward/create the host branch
jailbee git pull <name>       # fetch + merge into the container's BASE branch

jailbee git pull --current merges into the host's checked-out branch instead. The transport is git's ext:: helper over incus exec — no daemon, port, key or network involved.

The git bridge

How do I send a host branch into a container?

jailbee git push [<name>] [--merge|--rebase|--plain]. --merge/--rebase apply the pushed ref to the container's branch (conflicts are left for jailbee shell); --plain is transport only. With no name on a TTY you get a multi-select picker; with exactly one eligible container JailBee prints what it is about to do and waits for confirmation.

The git bridge, Confirming an auto-picked container

jailbee git push sent something I didn't expect — why?

Two defaults do the surprising part. The source defaults to the container's base branch (push.default_source), and JailBee pushes the origin/ copy of it rather than your local ref (push.push_from), because the local branch of a base you never check out is stale precisely when you just fetched. --from-local sends the local branch, --current sends the checked-out one.

Which copy of the source branch travels

What is the "base branch" and how do I change it?

Every clone-mode container records the branch it forked from (user.jailbee.base_branch, plus a refs/jailbee/base/<base> anchor). jailbee ls's AHEAD counts, jailbee git diff and jailbee git pull are all measured against it. Re-point it afterwards with jailbee git retarget <name> <base> [--merge].

The git bridge

Do submodules work?

Yes, in both directions and without a round trip through the submodule's upstream. On jailbee new they are initialised recursively and offline from the read-only host-source mount; on push/pull/checkout their objects travel over the same transport, and a sub-repo the peer lacks is created there first. Conflicting gitlinks are merged for you where possible, with a report of what is left. jailbee branch <branch> puts the whole tree — superproject and submodules — on one branch locally, which is how you jump back to master and out again in one command. jailbee submodule checkout is a hidden alias kept for compatibility; it prints a pointer to jailbee branch.

One limitation: the transport enumerates the sender's checked-out state, so -b <branch> on jailbee git merge / fetch / pull — which reads a branch the container does not have checked out — does not carry that branch's submodules. Check the branch out in the container first and run the command without -b.

Submodules

How do I review someone's pull request?

jailbee new --pr 1234

The PR head is fetched into refs/jailbee/pr/1234/head and checked out in the container — your own branches are untouched. Requires the gh CLI and gh auth login; fork PRs work. Pull in commits the author pushed later with jailbee git push <name> --pr --rebase (the fetch runs on the host, so no net loose is needed).

Reviewing a pull request, Round-tripping a PR container

How do I open or update a PR from a container?

jailbee pr <name> opens a draft PR, or pushes new commits and optionally regenerates the description when one exists. With Claude enabled the title, body and head branch name are AI-generated (--no-ai opts out). On a PR JailBee did not create it stays hands-off: the description is never regenerated unless you ask, and --force asks a second time.

Commands, A branch that already has a PR

How do I open a PR for a submodule?

jailbee submodule pr [<name>] [<path>] — a separate command from jailbee pr, because a submodule is a separate GitHub repository. Without <path>, the submodule with commits ahead of its own base is targeted automatically; several ahead means naming one. The signal is the submodule's own base anchor, not the superproject's gitlink diff, so it sees the commits even before you've committed the gitlink bump in the superproject. Base and head branch names come from the submodule's own .gitmodules/remote data, not the superproject's. Merge the submodule PR first — the superproject PR's gitlink bump then points at a merged commit.

Submodule pull requests

How do stacked PRs work?

Base the second container on the first one's branch (jailbee new feat/b feat/a). The host stays the hub and chain maintenance is merge-based — never rebase a branch with work stacked on it. When PR1 merges, jailbee git retarget feat-b main --merge flips B's base.

Stacked PRs

Three containers, one branch — where do I resolve the conflicts?

In a container, not on the host: the host is the one place with no test suite, no lint gate and no agent. jailbee git merge feat-a feat-b --into feat-c merges each source directly into the target container, one at a time, stopping at the first conflict; resolve inside jailbee shell feat-c, then jailbee git pull feat-c --current onto the host.

Merging several containers through one

How do I merge one container's branch into another?

jailbee git merge <source…> --into <target> — objects travel source → host → target without a host checkout, and no host branch, index or superproject working tree is touched (a host sub-repo can still be created, for a submodule born in the source container). Run it bare (jailbee git merge) and it asks for the sources, then the targets, each prompt a checkbox hiding whatever the other end holds — a container cannot merge into itself. Repeat --into for several targets; each takes every source. Sources run one at a time in the order given and stop at the first conflict, but only for the target they were running into: every target prints what landed, what didn't and the resume command, and a multi-target run closes with a roll-up. --plain transports the refs without merging. jailbee merge is a top-level alias for it.

Merging one container into another

Network and ports

What is the difference between strict and loose?

strict is a default-deny kernel ACL — only egress_allow destinations are reachable. loose permits all egress over a dedicated jailbee-loose bridge. Switch per container with jailbee net strict|loose <name>; the initial mode comes from defaults.network (strict).

Networks

Why does git push / gh fail inside a container?

By design: github.com is not in the default strict-mode allowlist, so day-to-day work runs offline-of-GitHub and an unattended agent cannot produce a surprise push. Either bring the commits to the host (jailbee git checkout <name>git push), or switch to loose for the write and back again.

Git remote & push

How do I allow a host through in strict mode?

For a host the whole team needs, add it to egress_allow as host[:port] (a CIDR or IPv4 also works) and run jailbee apply --no-restart. Prefer the host:port form — a port-less entry allows any protocol and port. Hostnames are resolved into the ACL and re-resolved every minute into a cumulative IP pool, so CDN addresses that rotate keep working; a hostname that fails to resolve aborts the whole apply.

For a host only you need, or one you want to try before committing it, jailbee net egress add <host>[:<port>] [<container>] widens that one container's allowlist, and on a strict container it takes effect at once (on a loose one it is stored and applies when the container returns to strict). --repo widens every container of the repo on this machine instead, pushed by the next jailbee apply. Neither touches config.yaml — container entries die with the container, repo entries are host-local state. jailbee net egress export prints the config key back with those overrides folded in, for when one has earned a place in git.

egress_allow, Egress overrides

Does loose stay on until I remember to switch back?

No — by default it auto-reverts after 5 minutes. Pick the TTL per switch with jailbee net loose <name> --for 2h, or opt out with --for never / --no-revert. With neither flag on a TTY JailBee asks. The remaining TTL shows in jailbee ls and jailbee net status.

loose_auto_revert

How do I reach a host service (e.g. adb) from inside a container?

Declare it once for every container of the repo:

host_ports:
  - { name: adb, port: 5037 }

Then plain adb devices works inside. jailbee port to-container / to-host / rm add or remove a single forward on one container without touching the config. A forward is a deliberate hole through the strict-mode boundary — it bypasses the ACL because Incus's proxy device never traverses the bridge the ACL is attached to. jailbee net status lists them.

Talking to Android devices over adb, Port forwards

Coding agents

How do I get Claude Code into my containers?

In ~/.config/jailbee/global.yaml:

agents:
  claude:
    enabled: true
    autostart: true      # launch it in a tmux window on every start

Then jailbee apply and create a container. The Anthropic hosts are added to the strict allowlist automatically, and JailBee's own skills are copied in so the in-container Claude can drive jailbee.

Claude Code in the container, Claude

Does it read my host ~/.claude?

No. <shared_dir>/claude is mounted as ~/.claude, so one login, settings, MCP servers and agents are shared across the repo's containers and survive jailbee destroy / jailbee new. Claude Code's global config (.claude.json) lives inside that mount: the golden image exports CLAUDE_CONFIG_DIR=$HOME/.claude, and Claude Code reads (CLAUDE_CONFIG_DIR || $HOME)/.claude.json. Claude runs its onboarding once, inside the first container.

Claude Code in the container

Can I really run an agent with permission prompts off?

That is the intended mode inside a container: you size the blast radius once in the config instead of adjudicating it prompt by prompt. Know what you sized — the container's clone, everything in host_mounts, and the shared state layer (including Claude's own credentials) are reachable; your host filesystem, unattached optional_mounts, private keys and every off-allowlist host are not. Stay in strict and snapshot before a long run.

Running an agent without prompts

Can I use an agent other than Claude Code?

Yes — agents: is a mapping keyed by agent name and wires any terminal coding agent into the same mount/egress/install/autostart pipeline. Six presets ship (claude, codex, gemini, aider, opencode, grok), but only claude is exercised in production — the other five are untested starting points you are expected to correct. You can also define an agent from scratch.

Generic agent support, Writing your own agent

I enabled an agent, but its tmux window exits with 127

Install happens only at jailbee new. Enabling an agent for an existing container and running jailbee apply attaches the mount and widens egress, but never installs the binary. Recreate the container, or install it by hand inside.

What this does

How do I make gh work for an agent inside a container?

Put a fine-grained PAT per GitHub owner in ~/.config/jailbee/global.yaml under github.api_tokens, keyed by each repo's container_prefix. The github block is rejected in a repo's .jailbee/config.yaml so tokens can't leak via git. chmod 600 the file; jailbee doctor warns about loose permissions and classic (ghp_*) tokens.

GitHub CLI (gh) inside containers

Desktop apps and dashboards

Can I run the IDE and a browser from inside the container?

jailbee ide <name> launches a JetBrains IDE; jailbee chrome <name> [URL] and jailbee firefox <name> [URL] launch a browser onto your Wayland session (jailbee browser launches whichever one is browsers.default, or the single enabled one). All are opt-in (jetbrains.enabled, browsers.chrome.enabled, browsers.firefox.enabled — personal settings that belong in global.yaml). Only one IDEA-family IDE runs at a time across containers (shared profile); Chrome and Firefox each run per-container from their own profile pool (jailbee pool ls/prune chrome-profile / firefox-profile, or the deprecated jailbee chrome-pool ls/prune alias for the Chrome one). Beyond the two built-in browsers, an apps: entry (an AppImage, a vendor binary, a wrapper script) launches with jailbee apps run <name> — see apps. Firefox defaults to browsers.firefox.source: image, so turning it on for the first time also needs a jailbee base build before it's actually in the image — not only when later changing source.

Limitations, Configuration reference

Is there a graphical dashboard?

jailbee gui (or jailbee dashboard --gui) — the Qt counterpart to the TUI, requiring the gui extra. It detaches to the background by default and logs to /tmp/jailbee-gui.log; --foreground keeps it attached. Interactive actions open in a host terminal emulator ($JAILBEE_TERMINAL forces one).

jailbee gui

How do GPG signing and SSH work inside a container?

JailBee shares the host agent's socket, not the key: with gpg.enabled the host gpg-agent socket is attached and SSH_AUTH_SOCK points at its SSH socket, so git commit -S and ssh work inside while the private key never leaves the host — a smartcard can still demand a touch per signature. ssh.enabled seeds only config, known_hosts and config.d/; private keys and authorized_keys are never seeded.

Security model, Sharing host sockets

Configuration

Where does configuration live, and which file wins?

Two layers, deep-merged: ~/.config/jailbee/global.yaml (personal, every repo) and <repo>/.jailbee/config.yaml (checked in, shared with the team). Scalars from the repo layer win, lists append, and [] in the repo resets a list to empty. Inspect with jailbee config show --layer global|repo|effective.

Configuration layers, Merge rules

What belongs in the global file and what in the repo file?

Personal things — UID/GID, credential mounts, IDE and Chrome preferences, GitHub tokens, agent API keys — go global. Repo-shaped things — stacks, autostart, resource limits, repo-specific egress — go in the repo file. All fields are technically legal at either layer; the split is convention, except that github is rejected at the repo layer outright.

Recommended placement

How do I add language toolchains or extra apt packages?

The golden image is stack-neutral by default. Turn runtimes on with golden.stacks (which also wires up the matching shared caches), and add system packages with golden.extra_apt_packages:

golden:
  stacks: { node: 22, docker: true }
  extra_apt_packages: [postgresql-client]

Then jailbee base build.

Stacks, Optional — stack runtimes

How do I run setup steps or start services automatically?

autostart.on_create fires on jailbee new, autostart.on_start on jailbee start. Each step is a shell command run as the dev user, with optional working_dir, background, per-step mounts and network. Auto-launching the IDE or a GUI app is configured outside that block, via jetbrains.autostart, browsers.<name>.autostart and apps.<name>.autostart.

Define autostart steps, autostart

How do containers of the same repo share caches and credentials?

Through <shared_dir> — one host directory per repo. Most entries are bind-mounted read-write into every container of the repo, so a package-manager cache stays warm across branches and settings written in one container appear in the next. A few caches — Gradle and Maven by default, since their tools take an inter-process lock on the cache directory — are instead pooled: each container gets its own private slot, seeded from the warmest existing one rather than shared live, so two containers never contend on one lock file. Either way it outlives jailbee destroy / jailbee new, and it is a layer of its own, so nothing a container does reaches your host dotfiles.

shared_caches, pooled_caches, shared_dir

How do I stop two repos from colliding on this host?

Every JailBee-owned Incus resource is prefixed with <container_prefix>-, which defaults to the repo directory name and must match [a-z0-9][a-z0-9-]*. Set container_prefix: explicitly if your directory name has capitals, dots or underscores.

Configure, container_prefix

Is there a quick way to pass a file between host and container?

Yes — if a .local/ directory exists at the repo root it is bind-mounted read-write into each new container at ~/<container_prefix>/.local, and it is added to the clone's .git/info/exclude so it never shows up as untracked. Presence-triggered and never auto-created; disable with share_local: false.

Sharing files with .local

When something breaks

Where do I start?

jailbee doctor, from inside the repo. It checks host and config and names most problems — uid delegation, bridge reachability, keyring quota, Incus reachability, GitHub token shape — with a remediation hint.

Start with jailbee doctor

A container gets no IPv4 address

A host firewall is blocking DHCP/DNS or forwarding on the JailBee bridges. Add the firewalld zone entries, or the UFW route rules plus the three before.rules lines per bridge. With the container left running, jailbee doctor's network <bridge> reachability check tells the three missing openings apart — no lease, no DNS, or no forwarding — and names the rule to add. It needs a container on the bridge to read a symptom from, so on a fresh host it stays silent until one is running.

Containers get no IPv4 address, Host networking

"disk quota exceeded" when starting a container or Docker

That is the host kernel keyring quota, not disk space — it runs out after a handful of concurrent containers. Raise kernel.keys.maxkeys and friends via /etc/sysctl.d/.

Kernel keyring limits

How much disk is all this using?

jailbee disk-usage for the breakdown, jailbee base usage for the golden images and their dated archives, and jailbee base prune to remove superseded archives (the live base is always kept).

Commands

How do I remove JailBee?

There is no jailbee uninstall; teardown is manual and ordered — per-repo resources first (containers, profiles, ACL, image, shared dir), then host-wide ones (registry mirror, the jailbee-loose bridge, the CLI itself) once the last JailBee repo is gone.

Removing JailBee