Use when running or explaining day-to-day `jailbee` (`jb`) commands against an already-set-up repo — creating/entering/destroying branch containers, the host↔container git bridge (`jailbee git push`/`pull`/`fetch`/`checkout`/`diff`), network modes (`jailbee net strict|loose`), egress overrides (`jailbee net egress ls|add|rm|export`, short alias `jailbee egress`), port forwarding (`jailbee port ls`/`to-container`/`to-host`/`rm`), the optional remote SSH service (`jailbee remote ssh`), `jailbee...
Installs into .claude/skills of the current project.
Are you the author of Jailbee Usage?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/vrtfinland-jailbee-usage)
---
name: jailbee-usage
description: Use when running or explaining day-to-day `jailbee` (`jb`) commands against an already-set-up repo — creating/entering/destroying branch containers, the host↔container git bridge (`jailbee git push`/`pull`/`fetch`/`checkout`/`diff`), network modes (`jailbee net strict|loose`), egress overrides (`jailbee net egress ls|add|rm|export`, short alias `jailbee egress`), port forwarding (`jailbee port ls`/`to-container`/`to-host`/`rm`), the optional remote SSH service (`jailbee remote ssh`), `jailbee dashboard`, `jailbee config edit`, `jailbee litellm up|down|status|login|logout|logs` and in-container `claude-jb`, snapshots, mounts, `jailbee ide`/`jailbee chrome`/`jailbee firefox`/`jailbee browser`/`jailbee apps ls`/`jailbee apps run`/`jailbee exec --detach`, background ops, reviewing PRs with `jailbee new --pr`, opening/updating PRs with `jailbee pr`/`jailbee submodule pr`, publishing an in-container agent's staged review comments with `jailbee review apply|ls|show|drop` and issue actions with `jailbee issue ls|show|apply|drop|resolve`, browsing both with `jailbee outbox`, stored agent logins and credential groups (`jailbee account`, `jailbee account group`), the shared RDP display (`jailbee display up|down|status`), the optional work network (`jailbee net migrate`), host-local config (`jailbee config edit --local`, `jailbee config migrate`) and silencing advisories (`jailbee dismiss`). Trigger on "how do I use jailbee", "jailbee new/shell/git/net/port/dashboard/config edit", "jailbee litellm", "claude-jb", "jailbee remote ssh", "connect to Jailbee over SSH", "how do I use gie", "gie new/shell/git/net/port/dashboard" (`gie` was jailbee's pre-1.0 command name, removed in 1.1.0 — users may still say it out of habit), "edit jailbee config interactively", "jailbee config edit keys", "spin up a container for this branch", "push/pull/merge the container branch", "switch the container to loose/strict", "allow this container to reach X", "add a host to the allowlist", "why can't the container reach X", "forward a port into/out of the container", "expose adb inside the container", "review this PR in a container", "open a PR for a submodule", "publish this submodule's commits as a PR", "post my review comments", "apply the review", "jailbee review ls/show/drop", "what's pending in the PR outbox", "luo kontti tälle branchille", "vie/tuo muutokset kontista", "välitä portti konttiin", "salli kontille pääsy hostiin", "lisää host sallittujen listalle", "avaa PR alimoduulille", "vie alimoduulin muutokset PR:ksi", "postaa katselmointikommentit", "julkaise katselmointi", "jailbee account ls/use/park", "jailbee account group", "jailbee claude ls/use/park" (deprecated alias of `jailbee account`), "jailbee issue apply", "apply the staged issues", "jailbee outbox", "what has the agent staged", "jailbee net migrate", "jailbee config migrate", "julkaise issuet", "switch which account the container uses", "switch the Claude account", "change which Claude login the container uses", "store this login", "store this Claude login", "vaihda tili", "vaihda Claude-tili", "mikä tili kontissa on käytössä", "mikä Claude-tili kontissa on käytössä", "jailbee apps", "jailbee browser", "jailbee firefox", "launch a GUI app in the container", "run a command in the background in the container", "käynnistä selain kontissa", "avaa gui-sovellus kontissa". For first-time repo configuration instead (writing `.jailbee/config.yaml`, `install.d/` snippets, golden-image tailoring) use the jailbee-repo-setup skill.
---
# Using JailBee day-to-day
`jailbee` (short form `jb`) runs many isolated dev environments in parallel
on one Linux host. Each environment is an **Incus system container** cloned from a
prebuilt "golden image", carrying its own backend, frontend, Docker daemon, IDE,
and browser — no port/name/schema collisions between branches.
`gie` is the pre-1.0 name of this tool. The `gie` command was **removed in
1.1.0** and no longer exists — always use `jailbee` (or `jb`). If a user types
`gie`, tell them the command is now `jailbee`; if a repo still keeps its config
in `.gie/`, that directory is still read (deprecated, removed in 2.0.0) and the
fix is `git mv .gie .jailbee`.
This skill is for **using** an already-configured repo (one that has
`.jailbee/config.yaml` and where `jailbee init` + `jailbee base build` have already run). If
the repo isn't set up yet, or you need to change `.jailbee/config.yaml`, use the
**jailbee-repo-setup** skill instead — unless the user just wants a quick,
throwaway container with no committed config at all, in which case `jailbee
new` already works as-is: see [Working without a repo config](#working-without-a-repo-config-scratch-directories)
below.
Goal: answer almost any "how do I do X with JailBee" question and run the right
command without falling back to `jailbee --help` for every step. The full flag-level
reference lives in [`references/commands.md`](references/commands.md) — read it
when you need an exact flag or an edge case this page doesn't cover.
## Mental model (read this first — it explains every command)
Four ideas make the whole tool make sense:
1. **One container per branch.** `jailbee new feat/foo` provisions a container,
clones the repo into it, and runs the repo's autostart steps. Slashes in the
branch become dashes in the **container name**: `feat/foo` → container
`feat-foo`. Incus resources are prefixed per-repo (`<container_prefix>-feat-foo`),
so two repos coexist; you can always pass the **short** name (`feat-foo`) and
JailBee resolves it.
2. **Clone mode vs mount mode.** Default is *clone*: the host repo is
`git clone --shared`'d into `~/<container_prefix>` inside the container (e.g.
`~/SampleApp`), giving the container its own working tree isolated from the
host. *Mount* mode (`jailbee new <name> --mount`) bind-mounts the host working
tree RW into the container instead — host and container share one tree. The
git bridge below works only in clone mode (mount mode shares the tree, so
there's nothing to bridge).
3. **The git bridge: the container is a git remote.** Because the container has
its own clone, commits move between host and container over a tiny bridge
instead of GitHub. `jailbee git fetch/checkout/pull` pull commits *from* the
container; `jailbee git push` sends commits *to* the container. Each container
records the **base branch** it was forked from (`user.jailbee.base_branch`, set at
`jailbee new` time) — pulls merge into that base by default, and `jailbee ls`/`jailbee git
diff` measure "how far ahead" against it.
4. **Network modes are a safety boundary.** Containers run **strict** by default:
a kernel egress allowlist blocks everything except what `.jailbee/config.yaml`
lists. **`github.com` is deliberately NOT allowed in strict mode** — so an
unattended agent can't surprise-push. To push/fetch/use `gh`, switch to
**loose** (full NAT) for the operation, then it auto-reverts.
## The daily loop
```bash
jailbee new feat/foo # create container off the default branch, autostart everything
jailbee ls # see what's running + each container's git status
jailbee shell feat-foo # drop into a shell (lands in ~/<repo>, the clone)
# ... work, commit inside the container ...
jailbee git pull feat-foo # merge the container's branch back into its base (e.g. main)
jailbee destroy feat-foo --force
```
`jailbee dashboard` (alias: `jailbee tui`) is the live, cross-repo version of
`jailbee ls` — an auto-refreshing TUI where you navigate containers and press Enter to act
(shell/ide/chrome/restart/stop/destroy, plus the workflow verbs — including
"Refresh from PR head" on a review container). Reach for it when juggling several
containers; reach for `jailbee ls` for a one-shot snapshot or scripting (`-o json`).
A repo with no `.jailbee/config.yaml` (a scratch directory) is listed and acted
on exactly like a configured one — no `--config` flag to pass, since the
dashboard runs each action's child process from that repo's own root.
`jailbee gui` (== `jailbee dashboard --gui`) opens the same dashboard as a graphical Qt
window instead of a terminal TUI. Its **View** menu switches between a wide
Table layout and a width-adaptive Cards layout (the default on a fresh
install), and within Cards, between a denser **Compact** style and a
**Grid** style; per-repo card groups are collapsible (click the group
header). The chosen layout, card style, collapsed repo groups, table column
widths/order, and refresh cadence / paused state persist across sessions
(window size/position do not).
Both dashboards include known repositories with no containers by default;
toggle **Show empty repos** to hide or restore all such groups, or hide an
individual repository by prefix. In the TUI, use **Settings > Visibility**;
in Qt, use **View > Repositories**. The Qt repository menu tracks repositories
as they become known, including hidden ones.
Both dashboards can also *create* a container. In the TUI, `n` asks for a
branch name and a base branch (pre-filled with the branch that repo's host
checkout is on) and then runs `jailbee new` in the terminal, so its own
questions — reusing an existing branch, and a branch autostart config that
widens network access — still get asked. The Qt dashboard does the same from
`&Container → New…` (Ctrl+N) or a right-click on a repo group header, opening
a terminal window for the run. Only those two fields are asked; network,
memory, cpu, mount and autostart come from the repo's config exactly as they
do for `jailbee new <branch> <base>` on the command line.
For review containers, select **New from PR…** in the TUI repo menu or the
Qt **Container** menu / repo header (the Cards view also has a **PR…** button).
Enter a positive PR number; the dashboard runs `jailbee new --pr N` in that
repo and leaves the CLI's confirmations intact. A repo must have a usable
directory; orphan groups cannot create either kind of container.
An empty but registered repository can be targeted from its header: press `n`
in the TUI, or use **View > Repositories** to restore it and then choose
**Container > New…**, or **New…** on the Qt repository group header. Orphan
groups remain view-only and cannot create containers.
## Creating containers — `jailbee new`
`jailbee new` figures out what to clone from whether `<branch>` already exists
upstream and whether you pass a `<base>`:
| Invocation | Result |
|---|---|
| `jailbee new feat/x` | branch doesn't exist → clone default branch, `checkout -b feat/x`; base = default branch |
| `jailbee new feat/x` | branch exists → clone it as-is (review/test an existing branch); base = default branch |
| `jailbee new feat/x feat/base` | branch doesn't exist → fork `feat/x` off `feat/base` |
| `jailbee new feat/x feat/base` | branch exists → clone it as-is with **base** `feat/base` (asks first; `-y` skips) |
| `jailbee new feat/x --current` | same, with the host's currently checked-out branch as the base |
| `jailbee new --current` | use the host's current branch as the work branch |
| `jailbee new mybox --mount` | mount mode — positional is the **container name**, not a branch; host tree bind-mounted RW |
| `jailbee new --pr 1234` | review PR #1234 (fetches the PR head, see "Reviewing a PR") |
The `<base>` positional always names the container's **base branch** — the
branch `jailbee git pull` merges into and the target branch status resolves on
the host. The bridge anchor is still used by transport operations, but status
uses the host's live local branch (falling back to its last-fetched upstream
tracking ref). Forking off it is merely what happens when the work branch does
not exist yet. So the way to put an
existing branch on the right base is `jailbee new <existing-branch> <base>`; use
`jailbee git retarget` only to change a base after the fact.
Missing refs fail fast with the exact `git fetch` to run — JailBee does **not**
silently auto-fetch a base you don't have. A base that exists only as
`origin/<base>` is fine. By default it clones the *upstream* tip
(`origin/<default>`), not your possibly-stale local branch
(`new.clone_from`/`new.autofetch` control this).
In clone mode, `jailbee new` reads **only** the `autostart` section from the
target branch's committed `.jailbee/config.yaml` at the exact commit it clones —
every other config key still comes from your checkout. A deviation prints a
diff naming the ref or commit it read.
Privileges are checked separately, against the repo's reviewed baseline
(`refs/remotes/<upstream-remote>/<default_branch>`) rather than your
checkout — so a
checkout lagging the default branch is not treated as an escalation. A step
attaching an `optional_mounts` entry the baseline's same-named step doesn't
**always** asks for confirmation (defaulting to no; declining creates nothing).
A step widening network access from `strict` to `loose` asks only for an
untrusted head — a `--pr` whose head lives in a **fork**; an internal PR is a
branch in your own origin, so it warns and proceeds like any other branch.
`--yes`/`-y` accepts up front, on top of its existing job of skipping the
"branch already exists" prompt above. With `--background` the question is asked
*before* the run detaches, in your terminal; the answer is pinned to the commit
you were shown, and if the branch moves in between the worker aborts naming
that instead of provisioning an unseen config.
No committed branch config, or one that doesn't validate, falls back silently
(or with a warning) to your checkout's autostart. `--mount`/`--no-clone` are
unaffected, `--no-autostart` skips the read entirely (no steps run), and `jailbee
start`/`restart`/`apply` never read the branch — only creation does.
See [Configuration](../../config.md#where-does-the-autostart-config-come-from)
for the full diff format.
Useful flags: `--no-autostart` (skip the repo's autostart steps — fastest, lowest
risk), `--no-clone` (bare container, no repo), `--name` (override the derived
container name), `--memory`/`--cpu`/`--net` (one-off resource/network overrides),
`--background`/`-b` (provision detached, see below), `--tmux`/`--shell` (attach
to tmux / a shell once it's up; forces foreground), `--wait`/`--no-wait`
(override an `autostart` stage's `detach: true` for this run — see
[Background operations](#background-operations) below).
Submodules come along automatically (offline) and round-trip through pull/push;
the host repo's submodules must be initialised first or `jailbee new` hard-fails with
the fix.
## Working without a repo config (scratch directories)
`jailbee new` (and every other command) works in a directory with **no**
`.jailbee/config.yaml` — no `jailbee config init` or `jailbee init` needed
first. Unless the host's `global.yaml` sets `scratch.enabled: false`,
JailBee synthesizes a config from that file's `scratch:` block, creates the
directory's Incus profiles on the fly, and registers it exactly like a
configured repo. `container_prefix` is still derived per-directory
(slugified from the directory name), but the golden image is pinned to one
alias — `jailbee-scratch-base` — shared by every scratch directory on the
host, so it's built once, not once per directory.
Four things to know before running `jailbee new` in a directory you haven't
seen configured before:
- **The first scratch directory on a host may prompt to build the shared
image.** If `jailbee-scratch-base` doesn't exist yet, `jailbee new` asks
to build it (a few minutes, one time); off a TTY it errors instead, naming
`jailbee base build` to run manually. Every later scratch directory finds
the image already there and skips straight to creating the container.
- **A non-git directory needs `--mount`.** Clone mode has nothing to clone
from outside a git repo, so `jailbee new work` in a plain folder fails
with a pointer at `jailbee new --mount work` — mount mode bind-mounts the
directory into the container instead.
- **It prints a one-line notice** naming where the values came from
(`global.yaml (scratch.config)`) and which base image is in use, so it's
never a silent surprise which config a container actually got.
- **A name clash with a *configured* repo is refused.** If the directory
name slugifies to a `container_prefix` a repo with a real
`.jailbee/config.yaml` already owns, `jailbee new` refuses and names both
directories — taking that registration over would silently re-render the
configured repo's egress allowlist from scratch defaults. The fix is
`jailbee config init` here with an explicit `container_prefix:`, or a
rename. Two *scratch* directories with the same name still share a prefix
(see below).
`jailbee config show` / `jailbee config validate` both work here too, and
report the same synthesized source instead of erroring on the missing file.
The one command that still needs a real config file is `jailbee net egress
export`: it prints a replacement for the repo config's `egress_allow:` key,
and there is none here to replace. Refused directories: `/`, `$HOME`, and any
ancestor of `$HOME` (`/home`, `/Users`).
For work that outlives an afternoon, point the user at `jailbee config
init` (the **jailbee-repo-setup** skill) instead of leaving it on scratch
defaults shared with every other directory on the host — see
[`scratch`](../../config.md#scratch) for the full config block and its
limitations (two same-named scratch directories under different parents
share one prefix, exactly like two clones of one repo do).
## The git bridge — moving commits host ↔ container
This is the subtlest part; get the direction right and everything else follows.
All of these refuse on **mount-mode** containers (they share the tree — just use
git on the host). Top-level aliases exist: `jailbee pull`/`push`/`diff`/`fetch`/`checkout`/`retarget`/`merge` ==
`jailbee git pull`/`push`/`diff`/`fetch`/`checkout`/`retarget`/`merge`. `jailbee merge` was
withheld at first — that verb used to name today's `jailbee git pull` — but the
merge target is never inferred, so the old one-argument shape asks which
container to merge into instead of quietly merging into the host.
**Container → host (pulling the container's work back):**
- `jailbee git fetch <name>` — fetch into `refs/jailbee/<short>/<branch>`,
transport the submodule objects, then point the host branch **and every
submodule's branch of the same name** at what the container has — **without
switching the working tree**. A host branch that has diverged is left alone
with a warning.
- `--force` — overwrite a diverged host branch anyway; always refused for the
branch currently checked out.
- `--tags` / `--follow-tags` / `--no-tags` — override `pull.tags` (default
`reachable`) for this run: `all` tags, tags reachable from the fetched
branch (lightweight and annotated alike), or none. Same three flags on
`checkout`/`pull`/`push` below.
- Switch the tree onto what was just fetched with `jailbee branch <branch>`.
- `jailbee git checkout <name>` — fetch, then fast-forward (or create) the matching
host branch **and switch onto it**. Refuses on divergence and points you at `jailbee git pull`.
Always fast-forward-only — `pull.ff` does not reach this command.
- `--as <branch>` — land it on a differently named host branch (the default is
the container's branch, or its PR head when the container has one).
- `-b <branch>` — read a different branch **from the container**; it never
renames the host branch. Same meaning on `fetch`/`pull`/`push`.
- `--tags` / `--follow-tags` / `--no-tags` — override `pull.tags` for this run.
- `jailbee git pull <name>` — fetch, then **merge the container's branch into its base
branch** (`user.jailbee.base_branch`, e.g. `main`). This is the usual "I'm
done, integrate it" command. By default (`pull.ff: auto`) it fast-forwards
when the host branch is strictly behind and writes a merge commit
otherwise — the "Merge branch 'X' from container Y" line only appears in
the latter case, since git skips `-m` on a fast-forward.
- `--into <branch>` — merge into a different host branch instead of the base.
- `--current` — merge into the host's currently checked-out branch instead of the
base (mirror of `jailbee git push --current`); mutually exclusive with `--into`, and
errors if the host is in detached HEAD.
- `--ff` — fast-forward only; refuse if histories diverged. `--no-ff` — always
write a merge commit (the pre-1.4.0 default; set `pull.ff: never` to keep
it permanently). Neither flag reads `pull.ff` (`never`/`auto`/`always`).
- `--checkout` — if the target branch isn't currently checked out, check it out,
merge, then stay on it (refuses on a dirty host tree). A target that is
**not** checked out and fast-forwardable is always fast-forwarded at ref
level, regardless of `--ff`/`--no-ff`/`pull.ff` — that's a ref move, not a
merge.
- `--cleanup` / `--no-cleanup` — force or skip the post-merge destroy-container +
delete-branch steps (otherwise governed by the `pull:` config block, which can
`prompt`/`always`/`never` each step).
- `--tags` / `--follow-tags` / `--no-tags` — override `pull.tags` for this run.
- **No name + a TTY** → multi-select picker; pulls each selected container in
order and stops at the first failure.
**Host → container (sending host commits in):**
- `jailbee git push <name>` — send a host branch into the container's clone. Source and
action come from flags, from configured defaults (`push.default_source` /
`push.default_action`), or are asked interactively when those are `ask`.
- `--merge` / `--rebase` / `--plain` — after transport, merge/rebase the pushed
ref into the container's branch, or just transport it (`--plain`). Refuses on a
dirty container tree; conflicts leave the container mid-merge/rebase — resolve
inside `jailbee shell <name>`.
- `--ff` / `--no-ff` (with `--merge`) — `--merge` fast-forwards when the
container is already on the pushed branch, which `--pr` always is, and makes
a merge commit otherwise. When the two have diverged, that fast-forward is
impossible: you are shown both commit counts and asked whether to make a
merge commit instead. `--no-ff` answers yes up front, `--ff` refuses and
fails. Without a TTY the divergence is an error naming `--no-ff`.
- `--from <branch>` (default: host default branch) / `--current` (host's current
branch).
- `--pr` (PR containers only) — re-fetch the PR head from GitHub first, pulling in
commits the author pushed since the container was created.
- `--from-local` / `--from-origin` / `--fetch`/`--no-fetch` — which *copy* of the
source branch to send. By default JailBee fetches and pushes
`refs/remotes/origin/<source>`, because a local `refs/heads/<base>` only
advances on `git pull` and is therefore stale right after a plain
`git fetch`. `--from-local` sends the host's local branch as-is (use it when
the host has unpushed commits); `--current` always resolves locally, and
`--pr` bypasses the choice entirely (it pushes `refs/jailbee/pr/<N>/head`, so
`--from-local`/`--from-origin` are rejected with it).
Configured by `push.push_from` / `push.autofetch`.
- `--tags` / `--follow-tags` / `--no-tags` — override `push.tags` (default
`none`) for this run: `all` tags, tags reachable from the pushed branch
(computed with `git tag --merged`, not `--follow-tags`'s own git meaning,
so lightweight tags survive), or none. Applies with `--merge`/`--rebase`/
`--force` too. Never re-points a tag that already exists in the container,
and never reaches the GitHub origin — `jailbee pr` sends no tags, with no
flag to change that. A tag that already exists in the container pointing
elsewhere is not skipped — git rejects it, and that failure aborts the
whole push (the merge/rebase included) before it runs; container → host
`reachable` skips such a tag silently instead, since that leg is git's own
automatic tag-following rather than an explicit per-tag refspec. Move a
re-pointed tag with `git push --force` first if this happens.
- **No name + a TTY** → multi-select picker; source/action chosen once, applied to
all, failures don't stop the batch (summary at the end).
**Container → container (merging one container's branch into another):**
- `jailbee git merge <source…> --into <target>` — merge one container's branch
into another **without a host checkout**: objects travel source → host →
target, 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). The merge runs inside the target on whatever it has checked
out, so conflicts are resolved there, in `jailbee shell <target>`.
- `--into <target>` — never inferred. Repeat it for several targets; each
one takes every source. Omit it on a TTY and jailbee asks which containers
to merge into.
- Omit the sources too (`jailbee git merge`) and jailbee asks for those
first, then the targets. Both prompts are checkboxes, and the sources are
merged in the order the rows were **listed**, not the order they were
ticked — the prompt says so. With `-b` the source prompt is single-select,
since one branch cannot describe several sources; the target prompt stays
a checkbox. Off a TTY both ends must be given.
- **A container cannot be named at both ends** — merging a container into
itself would merge a branch into that same container's checked-out branch,
which its own `git merge` says better. Each prompt hides the rows the other
end holds, and a typed collision (`--into` naming a source) is exit 2
before anything is merged.
- `-b <branch>` — read this branch from the source container (only valid with
one source). **Not submodule-safe:** the submodule transport enumerates the
source's *checked-out* state, so a submodule that exists only on `<branch>`
(or a gitlink no local sub-repo branch reaches) never travels, and the
target's `submodule update` fails *after* the merge commit is written. For
a repo without submodules it is unaffected; otherwise check the branch out
in the container first and merge without `-b`.
- `--plain` — transport the refs only; run no merge. The summary then says
"transported", not "merged" — `--plain` is not a kind of merge.
- Several sources are merged into each target **one at a time, in the order
given**, and stop at that target's first conflict or failure — the next
source would land on a tree left in merge state. Targets are separate
containers, so **a target that stops does not stop the ones after it**.
Every target reports what landed, what stopped it, what was not attempted,
and the command to resume; a run with several targets closes with a
roll-up naming the state of each one.
- Each source that lands prints its own `── Submodules` block naming the
gitlinks that merge moved, read **inside the target container** (the
merge commit exists nowhere else, so the host cannot diff it).
```bash
jailbee git merge # pick the sources, then the targets
jailbee git merge c1 --into c4
jailbee git merge c1 c2 c3 --into c4 # one at a time, stop on conflict
jailbee git merge c1 --into c4 --into c5 # both targets take c1
jailbee git merge c1 # pick the targets only
jailbee git merge c1 --into c4 --plain # transport only
jailbee git merge c1 --into c4 -b feat/x # read feat/x from c1
```
**Inspecting the difference:**
- `jailbee git diff <name>` — by default the direct committed-tree diff against
the host's live target branch. `--incoming` selects the contribution-only
three-dot diff; `--wt` is working-tree-only, `--all` combines WT with the
default committed diff, and `--stat` summarizes either committed view.
Missing target refs or unreadable target objects produce a clear error.
`jailbee ls` surfaces the same picture per container without a diff: **BASE** (base
branch), **WT** (uncommitted changes), **DIFF ±** (direct committed-tree line
diff from the host target), **↑** (commits unique to the container), **↓**
(commits unique to the host target), and **MERGE**. Equal trees can still have
nonzero commit counts. Stopped and mount-mode containers show `—` in the git
columns.
Status snapshots the host's `refs/heads/<base>` once per branch and uses the
same SHA for all matching containers. If that local branch is absent, it falls
back to `refs/remotes/<upstream>/<base>` and marks BASE as tracking. If neither
exists or its selected commit object cannot be read, comparison-dependent
values are `?`—never a pinned-anchor or unrelated-default fallback. No fetch,
fast-forward, or anchor write happens during a listing. When both refs exist,
local wins; a tracking-ahead or diverged relation produces a notice describing
the last-fetched ref only, not the remote server's current state. Bridge anchors
remain for transport; container-to-container `jailbee git merge` does not
refresh the host target or trigger an anchor fan-out. The next status gather
after the host branch moves naturally reads its new tip.
The JSON `git_status` object exposes `target_diff`, `ahead_count`,
`behind_count`, `base_source`, `base_sha`, `tracking_relation`, and
`upstream_ref`. `ahead_diff` was removed; configured uses warn and explicit
`--fields ahead_diff` errors. Per-submodule `ahead_ins`/`ahead_del` are
replaced by `target_ins`/`target_del`; use `target_diff` for the direct line
diff or `jailbee git diff --incoming` for the former contribution view.
**MERGE**'s values, in priority order — a live state always outranks a
prediction:
| Value | Meaning |
|---|---|
| `conflict!` | Unresolved conflict in the container **right now** (unmerged paths). |
| `merging` / `rebasing` / `cherry-picking` / `reverting` | That operation is in progress — conflicts resolved, the commit is pending. |
| `conflict` | *Prediction only:* merging this branch into its base would conflict. Nothing is running. |
| `ok` | *Prediction only:* would merge cleanly. |
| `?` | The live state couldn't be probed. |
| `—` | No data — stopped or mount-mode container. |
So a container left mid-merge by `jailbee git push --current` reads
`conflict!`, not `ok`, even though a clean merge to base is still predicted —
that's the whole point of tracking the live state separately.
Two more git-status columns exist but are **off by default** (opt in with
`--fields` or the `ls:` config block — see [Configuration](../../config.md#ls--dashboard--remembered-columns)):
**LOCAL ±** (`local_diff`) and **L↑** (`local_count`) — the diff between the
container's HEAD and the *host's currently checked-out branch*, as opposed to
DIFF ±/↑/↓'s live target comparison. They show `?` when neither side happens
to hold the other's commit as an object — the probe never fetches or pushes to
force an answer out of a listing command. A `jailbee git pull` may make the
container's tip available on the host, but does not change which ref LOCAL
compares against.
The destroy guard's "commits not on the host" check (below) depends on the
container's HEAD sha and whether any remote-tracking ref contains it.
Neither is a `jailbee ls` column, but both appear in the `git_status` field's
JSON payload (`jailbee ls -o json`, keys `head_sha` / `remote_contained`) for
scripting.
**Two more bridge commands:**
- `jailbee git retarget <name> [<new-base>] [--merge]` — re-point a container at a
different base branch (rewrites `user.jailbee.base_branch`; `pull`/`push`/`ls`
follow it). Without `<new-base>` it asks with a branch picker on a TTY (error
off one); the dashboards' "Change base branch" entry relies on this. The stacked-PR tool: when a parent PR merges to `main`, retarget
its dependent container from the parent branch onto `main`. `--merge` does
**not** honour `push.tags` or `push.ff` — the merge it runs always behaves
as `none` for tags and always writes a merge commit for `ff`, and there is
no flag to change either.
- `jailbee branch [<branch>] [--container <name>] [--submodules-only]` — put the
tree on one branch, superproject and submodules, when they land on a detached
HEAD after clone/push/pull. No `--container` → the host repo; `--container
<name>` → that container. On the host, a BRANCH argument checks that branch
out in the superproject first and then aligns the submodules to it, so jumping
the whole tree back to `master` is one command; `--submodules-only` keeps the
superproject where it is. A container's branch is its identity, so BRANCH
never switches it there, and `--submodules-only` combined with `--container`
is rejected (exit 2). No `-c` short form: `-c` is `--config` on every jailbee
command. `jailbee submodule checkout` is a hidden alias kept for
compatibility; it prints a pointer to `jailbee branch`.
```bash
jailbee branch # host, align to current branch
jailbee branch master # host, whole tree to master
jailbee branch master --submodules-only
jailbee branch --container feat-foo # container 'feat-foo', its branch
jailbee branch master --container feat-foo
```
**Recipe — merging several containers through one.** Three features built in
parallel become one branch without resolving anything on the host, which is the
one place with no tests, no lint gate and no agent. The direct way, since all
three sources are themselves containers, is `jailbee git merge`:
```bash
jailbee git merge feat-a feat-b --into feat-c # one at a time, stop on conflict
# conflict? resolve inside container c, run the gates there, commit the merge
# (resume from wherever it stopped — the summary names the exact command)
git checkout main
jailbee git pull feat-c --current # all three land on main at once
```
The older, manual way — still what to use when a source is a plain host branch
with no container of its own — sends each branch through the target via
`push --current`:
```bash
jailbee git checkout feat-a # host HEAD → feat/a, taken from its container
jailbee git push feat-c --current # feat/a into container c, merged into its branch
# conflict? resolve inside container c, run the gates there, commit the merge
jailbee git checkout feat-b # repeat per feature
jailbee git push feat-c --current
git checkout main
jailbee git pull feat-c --current # all three land on main at once
```
`--current` is load-bearing there: `push`'s default source is the container's
*base* branch, so without it you would send `main` into c. The action must be
a merge or rebase — `plain` transports the ref without applying it, so no
conflict ever appears. Containers a and b survive the last pull and are
destroyed by hand either way.
Full version with the cleanup rules: [Git bridge](../../git-bridge.md#merging-several-containers-through-one).
## Network modes — `jailbee net`
```bash
jailbee net loose feat-foo # full NAT — e.g. for a fetch from an off-allowlist host
# ... push or fetch over the network ...
# auto-reverts to the previous mode after ~5 min (loose_auto_revert)
jailbee net loose feat-foo --for 2h # pick the TTL for this switch only
jailbee net loose feat-foo --no-revert # stay loose until switched manually
jailbee net strict feat-foo # back to the egress allowlist now
```
`loose` auto-reverts to the previous mode after a TTL (default 5 min, see
`loose_auto_revert` config). Per switch, `--for <dur>` overrides that default
(`30s`, `45m`, `4h` — max 24h; `--for never` = `--no-revert`, and the two flags
are mutually exclusive). With neither flag JailBee **asks interactively** — only on
a TTY, with `JAILBEE_NONINTERACTIVE` unset and the policy enabled; anywhere else the
configured `after` applies silently. In a script, pass `--for` or `--no-revert`
rather than relying on either behaviour. A disabled policy schedules nothing and
never asks, but an explicit `--for` is still honoured.
`jailbee ls` shows the remaining TTL while any container is loose. The
**no-push-from-strict gate is intentional** — don't add `github.com:22` to the
strict allowlist to "fix" a failing push; push from the host (`jailbee git pull`
then `git push`, or `jailbee pr`) or switch to loose for the op. `jailbee net
refresh` re-resolves the allowlist hostnames (useful after a CDN rotates IPs);
`jailbee net status` shows the refresh timer + per-repo pools.
**Two network generations.** By default strict containers sit on `incusbr0`
and loose ones move to the `jailbee-loose` bridge, so a mode switch replaces
the NIC and the address changes. `jailbee net migrate` (host-wide, asks first)
makes the `jailbee-work` network the default for **future** containers: a
fixed IPv4 address on one bridge, with strict/loose switched by swapping ACLs
on the NIC and loose egress scoped to the container's own address. Existing
containers stay where they are, and `jailbee net migrate --undo` makes legacy
the default again without moving anything. The work network is IPv4-only, and
switching back to strict does not cut a connection loose mode already opened.
## Egress overrides — `jailbee net egress`
For a host a container needs that isn't in `.jailbee/config.yaml`'s
`egress_allow`, and doesn't belong there (a one-off, not something the
whole team needs), add it without touching the config:
```bash
jailbee net egress add pypi.org feat-foo # this container only
jailbee net egress add pypi.org --repo # every container of this repo, this host
jailbee net egress ls feat-foo # what applies, and where each entry came from
jailbee net egress rm pypi.org feat-foo # undo it
```
Three facts that matter when explaining this:
- **Additive only.** An override can widen the strict-mode allowlist, never
narrow it — `config.yaml` is always the floor. `rm` refuses an entry that
exists only in `config.yaml`, pointing at the file instead.
- **Container scope is the default**, and dies with the container (stored
in its own label). `--repo` is the wider, explicit opt-in: every
container of the repo, on this host only.
- **Host-local, not committed** either way — repo-scoped entries are stored
in `~/.config/jailbee/repos/<container_prefix>.yaml`, never shared with the
team or seen by a reviewer or CI. That's also the risk: see
[docs/security.md](../../security.md#egress-overrides) before suggesting
one as a substitute for adding to `config.yaml`. If a host turns out to
be needed permanently, `jailbee net egress export` prints a paste-over
replacement for the config's `egress_allow:` key.
Also available as the short root alias `jailbee egress add|rm|ls|export`.
Full flag reference: [references/commands.md](references/commands.md#egress-overrides--jailbee-net-egress).
## The login is often shared between repos — `credentials`
Several repos on one host can share a **single login per agent** (Claude
today), and on a recently set-up host they usually do: a `global.yaml` written
by `jailbee config init --global` ships `credentials.group: default`, which
puts every repo on that host in one group. Hosts whose `global.yaml` predates
that key, or that set `group: null`, keep one login per repo.
Only the *credential* is shared. Each repo keeps its own `~/.claude`, so
project history, MCP config, sessions and skills never cross repos. Inside a
container of a member repo:
- `~/.claude` is this repo's own config home, as always
- `~/.claude-creds` is the **shared** directory, holding `.credentials.json`
and the rotation lock, and `CLAUDE_SECURESTORAGE_CONFIG_DIR` points at it
- this repo's own `~/.claude/.credentials.json` does **not** exist — joining
the group moved it out
You can tell from inside a container with `echo
$CLAUDE_SECURESTORAGE_CONFIG_DIR`: set means this repo is in a group, empty
means it keeps its own login.
**To change which account the group uses:** `/login` inside any member
container writes straight into the shared directory, and every other member
picks the new account up on its next Claude Code start. If the account is
already stored from an earlier `jailbee account park`, `jailbee account use
<email>` (see below) switches the whole group to it without a fresh login.
**"Why did my Claude account change?"** Almost always: someone ran `/login` in
a container of *another* repo in the same group. Point the user at the host —
`jailbee doctor` there names this repo's group and lists its other member
repos, and prints nothing at all when the repo shares no credential.
**The host-wide default** is `credentials.group` in
`~/.config/jailbee/global.yaml`; a repo-specific membership is
`credentials.group` in `~/.config/jailbee/repos/<prefix>.yaml`. Use
`jailbee account group set <name|none>` / `unset` to change this repo's local
choice. `jailbee config edit --local` edits the local file, and
`jailbee config show --layer local` inspects it. Two things about changing
membership are worth warning a user about:
- Joining **moves** this repo's credential into the group directory. If the
group already holds a login and this repo has one too, `apply` asks which to
keep and deletes the other — the two are independent grants, so the survivor
is unaffected, but the loser is gone and needs a fresh `/login` to come back.
- Leaving does **not** restore anything. The join moved the credential out, so
a repo that leaves has no credential at all and needs one `/login`.
Neither is reversible by jailbee, and neither can be run from inside a
container — there is no `jb` binary there.
Older `github.api_tokens`, `credentials.repos` and repo-scoped egress rows in
`state.sqlite` can be migrated with `jailbee config migrate`. It is a dry run
by default; review the diff, then pass `--apply`. Conflicts are left in place
for manual resolution. `jailbee dismiss legacy-per-repo-map` dismisses the
legacy-map notice after you've handled it.
## Switching which account is in use — `jailbee account`
A *holder* is whatever directory a repo's containers read a credential from:
the credential group directory when `credentials` puts the repo in a group,
otherwise the repo's own config home. One holder has at most one live login
*per agent*; every other stored login sits parked in a host-wide store.
```bash
jailbee account ls # every login on the host, and where each is live
jailbee account use me@work.com # switch this holder to a stored login
jailbee account park # store the current one; next agent run asks /login
jailbee account rm old@work.com # delete a stored login for good
jailbee account group ls # the groups themselves, and what each holds
jailbee account group create staff # an empty credential group, before anything uses it
jailbee account group rm staff # remove one nothing uses (parks any login it holds)
```
Five things worth knowing:
- **A switch is holder-wide.** In a credential group, every member repo moves
with it. `jailbee account ls` is host-wide: one row per login, with the group
it is live in, the repos sharing that holder, and the containers reading it —
including a container moved by `jailbee account group use`, which is the only
evidence a group no repo resolves to is in use.
- **`-a/--agent` picks the pool.** Every command acts on all enabled pooled
agents by default; with more than one, `-a <agent>` narrows it. A typed
account reference matching more than one agent's store is ambiguous, and off
a TTY the error names the `-a` values to pass. `account group` commands take
no `-a` — a group name is shared.
- **No restart.** Claude Code re-reads the credential when the file's mtime
changes, so a session that is open right now picks the new account up on its
next turn. The account shown by `/status` can lag until the session restarts;
authentication does not.
- **Adding an account is `park` then `/login`.** There is no `add`: only a
browser login creates a credential, and it lands in the holder by itself.
Logging in as an account that is *already* parked is fine — the two grants
are independent, so the new one is stored under a name with a `~<timestamp>`
suffix (`me@work.com~20260828-104233`). Pass that full name to `use`/`rm`
when a bare email is reported as ambiguous.
- **A login is never copied.** Every operation moves the file, because two
copies of one login share a refresh-token lineage and the first rotation
silently kills the other. That is also why `rm` is permanent.
If a container says "Not logged in" straight after a switch, run
`jailbee doctor` on the host: a holder with parked logins and no live one is
what a `park` leaves behind.
A login that has sat parked for a long time can also come back dead — Anthropic
can revoke the grant, and JailBee never contacts the token endpoint, so it
cannot tell in advance. The symptom is a `/login` prompt in the container right
after a switch that reported success. Logging in there fixes it, and the new
credential lands in the holder as usual.
## Host-wide agent instructions
`/etc/claude-code/CLAUDE.md` holds the host-wide instructions from the host's
`~/.config/jailbee/AGENTS.md` (`$XDG_CONFIG_HOME/jailbee/AGENTS.md` if set).
It is read-only in the container; to change it, edit that file on the host,
not the managed file or a shared `~/.claude/CLAUDE.md`. Run `jailbee ls` on
the host to refresh the copy; the next Claude session reads it, not an
already-running one. Existing repos need `jailbee apply` once and a restart
of running containers to add the mount. `agent_instructions: false` in the
host's `global.yaml` skips updates and removes the mounts after apply and
restart, but retains host staging. Only Claude is wired today.
## Claude Code through LiteLLM — `claude-jb`
On the host, enable `litellm.enabled: true` in `global.yaml`, then run
`jailbee litellm up`, `jailbee litellm login [ACCOUNT] [--provider chatgpt|xai]`
(ChatGPT device code; xAI in the host's browser, experimental), `jailbee litellm up` again (the first leaves an unlogged account
stopped), `jailbee base build` per repo and `jailbee apply` per repo. Inside a
container, `claude-jb` runs Claude Code through the proxy while plain `claude`
remains native. Choose a gateway profile with `claude-jb --profile NAME`, then
`JAILBEE_LITELLM_PROFILE`, then `litellm.default_profile` (`codex`), which a
repo's host-local override may change for its own containers. The host also
supports `jailbee litellm ls` (profiles and routes as `claude-jb` uses them,
globally and per repo override; read-only; allowed over remote SSH in the default commands mode, but refused when
`remote.ssh.excluded_repos` is set),
`jailbee litellm status`, `logs [ACCOUNT] [-f]`,
`logout [ACCOUNT] [--provider chatgpt|xai]` and `down [--purge]`. After `down`, run `jailbee apply` to
remove stale proxy settings from running dev containers. Several ChatGPT
accounts (`litellm.accounts`, one per profile via `profiles.<p>.account`) and
API-key providers (keys in the host's `~/.config/jailbee/litellm/secrets.env`)
are supported. A repo can override routes, profiles, `default_profile` and
`autostart` in its host-local `~/.config/jailbee/repos/<prefix>.yaml` under
`litellm:` (never in the committed repo config); edit it on the host with
`jailbee config edit --local`, then run `jailbee apply` (`jailbee new` alone
does not update the proxy; route and profile edits reload into the running proxy,
and `apply --no-restart` defers only restarts (secrets, `extra` outside its `model_list`, settings, an
unconfirmed reload); an edit that only changes egress, such as a route's `egress`
list on an existing route or `litellm.egress`, needs `jailbee litellm up` because `apply` does not
notice it). `claude-jb` gives Claude Code per-tier names such as
`jb.codex.capable` (`jb.<profile>.<most-capable|capable|standard|cheap>`), not
route names. After `apply`, a route rename or remap therefore reaches open
sessions without breaking them; renaming a profile or unmapping a tier does
break them. A profile's `instructions` (model-policy text written on the host) is appended to Claude Code's system prompt by `claude-jb`; it is set on the host, takes effect after `jailbee apply`, and cannot be changed through the host's config from inside the container. It is guidance, not enforcement: the container's copy is editable (sudo) until `jailbee apply` rewrites it.
With `litellm.autostart`, the Claude autostart window runs
`claude-jb`. Inside the container there is no key and no login: `claude-jb` only
reads `/etc/jailbee/litellm.json` and the proxy key for the profile's account.
Full setup and limits:
[LiteLLM](../../litellm.md); exact CLI flags:
[commands](references/commands.md#litellm-proxy).
## Port forwarding — `jailbee port`
A forward is an Incus proxy device wired directly into (or out of) the
container's network namespace — it bypasses the network ACL by construction,
so it works identically in **both** `strict` and `loose`, and doesn't need
`net loose` to set up or to use. `jailbee net status` prints an active
forward's own section ("Port forwards: N on M container(s) — the network ACL
does not see these"), separate from the allowlist/TTL info above.
The single rule that makes every invocation unambiguous: **the positional
argument is always the container-side port**, in both verbs, and
`--host-port` always names the host side. There is no `HOST:CONTAINER` colon
syntax anywhere.
The verb names the side a service becomes **available** on — not which end
opens the TCP connection. Those are opposite ends of the same forward, in
both directions:
- `jailbee port to-container PORT [NAME] [--host-port N] [--proto tcp|udp]`
— a **host** service becomes reachable **inside** the container. The
container listens on PORT; Incus's proxy connects out to
`--host-port`/host on the host. This is the adb case: the host runs the
adb server on `127.0.0.1:5037`, `jailbee port to-container 5037 <name>`
makes plain `adb devices` work inside the container, and the *container*
is the one that opens the outward connection even though the command name
says "to-container".
- `jailbee port to-host PORT [NAME] [--host-port N|auto] [--proto tcp|udp]`
— the mirror: a **container** service becomes reachable **on the host**.
The host listens (on PORT, unless `--host-port` says otherwise);
`--host-port auto` asks Incus/the OS for a free host port and prints the
one it picked. The *host* opens the connection inward, even though the
command name says "to-host".
- `jailbee port ls [NAME]` — list forwards; with no NAME, every container of
the repo. It lists **every** proxy device on the container, including one
added by hand with plain `incus config device add` — that one shows up
with source `other` rather than `config`/`ad-hoc`.
- `jailbee port rm HANDLE [NAME]` — HANDLE is a device name, a `host_ports`
config entry's `name`, or a container-side port number (rejected as
ambiguous if more than one forward uses that port).
Config-declared forwards (the `host_ports:` block in `.jailbee/config.yaml`
— see the jailbee-repo-setup skill) are always the to-container direction
and apply to every container of the repo. `jailbee port to-host` has no
config equivalent by design — it's per-container and ad hoc, because a host
listener is a machine-wide resource that containers of the same repo would
otherwise fight over.
## Remote SSH access
The optional SSH service exposes JailBee rather than a host shell. It needs the
`jailbee[ssh]` extra, an authorized public key, and explicit enablement:
```bash
jb remote ssh key add
jb remote ssh enable
jb remote ssh status
```
`key add` with no argument prompts and reads one pasted public-key line; a
file path (`jb remote ssh key add ~/.ssh/id_ed25519.pub`) and piped stdin
(`cat ~/.ssh/id_ed25519.pub | jb remote ssh key add -`) also work.
Administration is `jb remote ssh enable|disable|restart|status|serve`; key
management is `jb remote ssh key add|ls|rm`. `rm` takes the full SHA256
fingerprint printed by `add` or `ls`. Key edits apply to new connections
without a restart. `disable` preserves keys and config; `restart` is needed
after changing `listen` or `port`; `serve` is the foreground diagnostic path.
`serve` also takes one-off `--listen`/`--port`/`--dashboard`(`/--no-dashboard`)
/`--shell`(`/--no-shell`)/`--exec`(`/--no-exec`)/`--commands`/`--allow`
/`--restrict-host`(`/--no-restrict-host`)/`--files`(`/--no-files`) overrides of
`remote.ssh`, for trying a policy without editing `global.yaml` (never written
there, and the systemd unit never passes them); the persistent setting for
`sftp`/`scp` into a container's repo dir is `remote.ssh.files: true` in
`global.yaml`, and changing it needs `jb remote ssh restart`; `--allow`,
given at least once, replaces the configured `commands.allow` list rather
than appending to it, e.g. `jb remote ssh serve --port 18022 --shell
--commands allowlist --allow ls --allow new`.
Client forms at the default loopback endpoint:
```text
ssh -t -p 8022 jailbee@localhost dashboard
ssh -t -p 8022 jailbee@localhost shell [--repo PREFIX]
ssh -p 8022 jailbee@localhost -- --repo PREFIX COMMAND [ARGS...]
```
The `--` is for the client: OpenSSH keeps parsing its own options after the
destination while the next word starts with `-`, so a bare `--repo` fails with
`unknown option -- -`.
A commandless login prints the enabled forms and exits by default; set
`remote.ssh.default_entrypoint: dashboard` (or `shell`, if enabled) in the
host's `global.yaml` to open that entry point instead. `ssh jailbee@host help`
always prints the enabled forms. Dashboard and the
restricted JailBee console need `-t`. Every one-shot command requires an exact
registered `PREFIX`; `--repo` never accepts a path. Started without `--repo`,
the console shows an arrow-key menu of registered repos (skipped when exactly
one is registered); Esc/Ctrl-C/Ctrl-D cancel it. The console's local commands
are `repos`, `use [PREFIX]` (bare `use` reopens the menu), `dashboard` (shown
only when enabled), `help`, and `exit`, rendered by `help` as a Rich panel
styled like `jb --help`. In `full` mode `help` then runs the real `python -m
jailbee --help`; in `allowlist` mode it renders a second panel listing each
allowed command with its own short help instead; in `disabled` mode it prints
a one-line note. Every other line is a JailBee argv checked against
`remote.ssh.commands`, tab-completed word by word. A hidden alias (`merge`,
`pull`, `push`, ...) is checked against the public command it aliases, so
allowing `git merge` also allows `merge`. A public group's own help (`git`,
`git --help`) is allowed on its own whenever some command under it is
allowed, and so is the bare top-level `--help`. A name matching no command at
all is run anyway, so `jailbee` reports its own "No such command" error. It
has no shell operators, expansion or executable lookup. In every mode, `full`
included, a remote command may not set a path-typed option or argument
(`--config`, `net refresh --repo`, ...) nor `new --mount`: the policy picks
commands, never host paths. The remote dashboard has no config editor, diff
pager or GUI app launches. The git bridge moves refs only: `git checkout`,
host `branch`, and `git pull`/`fetch` into the host's checked-out branch are
refused (use `--into`/`--as`), a mount-mode container cannot be entered
(`shell`/`tmux`/`exec`), and a branch-autostart privilege widening is
refused even with `--yes`. Host-management commands (`config edit`/`init`,
`remote ...`, `setup`, `init`, `apply`, `base build`/`prune`, `net egress
add`/`rm`, `port to-container`, `mount`, writing `account` commands, the GUI
launchers) are refused in every mode, `full` included. Publishing
(`pr`, `review apply`, `issue apply`) stays allowed but never with `--yes`,
and `pr --web`/`--open` are refused.
`remote.ssh.restrict_host: false` (or `serve --no-restrict-host`) lifts all
of these at once.
The service runs as the same host UID as local JailBee, and every authorized
key has identical access to the configured surface across all registered
repos. By default, dashboard, console and one-shot execution are enabled on
`127.0.0.1:8022`; the shared command policy defaults to `full`, while host
restrictions remain on. Prefer exact-leaf allowlists for narrower command
access. `commands.mode: full` is high trust: in restricted sessions it admits
classified public commands and newly added or unclassified commands fail
closed. With `remote.ssh.restrict_host: false` and no inherited restricted
session marker, it admits public leaves including future ones except reserved
routes and hidden internal commands. See the full command behavior and security boundary in
[`references/commands.md`](references/commands.md#remote-ssh).
With `remote.ssh.gui: true` and `jailbee display up` run on the host, `jailbee
chrome`, `firefox`, `browser`, `ide` and `apps run` over an SSH session draw on one shared RDP
display (weston, one screen for every container) instead of being refused.
They print how to connect: tunnel `ssh -N -L 3389:127.0.0.1:13389 -p <port>
jailbee@<host>`, then an RDP client to `localhost:3389` (any login is
accepted). The tunnel can be opened before or after a launch; the launch waits
up to 120 s for a client. For an
arbitrary command use `jailbee exec <name> -d --gui -- <cmd>`; a plain `exec
-d` never touches the display. `display up|down` are host-only (not
available inside a container); `display status` works over SSH.
## Other day-to-day commands
- **Shell / run:** `jailbee shell <name>` (interactive, lands in the clone),
`jailbee tmux <name>` (attach the autostart tmux session), `jailbee exec <name> -- <cmd>`
(one-off, e.g. `jailbee exec feat-foo -- pnpm test`). If `<name>` is omitted where a
TTY exists, you get a picker.
- **Lifecycle:** `jailbee start|stop|restart <name>`; `start`/`restart` re-run
autostart, and both take `--background`/`-b` to detach that run and
`--wait`/`--no-wait` to override a `detach: true` autostart stage for this
run (see [Background operations](#background-operations) and
[Autostart stages that keep running after the hand-off](#autostart-stages-that-keep-running-after-the-hand-off)).
`stop` refuses while a detached autostart run is in flight (`--force`
skips the check); `restart` refuses too, with no `--force` — cancel the
run with `jailbee autostart cancel <name>` first. `jailbee destroy <name>
--force`, or `jailbee destroy --all` (whole repo,
one confirmation), or `jailbee destroy` with no args for an interactive checkbox.
Add `--background`/`-b` to detach. Before the usual confirmation, JailBee
assesses what destroying would discard — a dirty working tree, a changed
submodule, or commits that exist on neither the host nor a remote — and, if
anything is at risk, shows a one-line summary per container and a second
confirmation defaulting to **No**. Nothing fires it for work already pulled
to the host or pushed anywhere. Three outcomes: something at risk → the
summary and the second confirmation; a **running** container whose git
status could not be read at all → treated the same way, with the reason
"could not inspect the container" (unknown is never presented as safety);
a container that was never probed to begin with — the normal state for a
**stopped** one — just gets a note, no extra prompt (mount mode is exempt:
its working tree is the host's and survives the destroy). An unmeasurable
commit count (`AHEAD ↑` = `?`) still warns when the container's HEAD is on
neither the host nor a remote-tracking ref. `--force` skips both
confirmations and the assessment itself, on the single-name, `--all` and
interactive-picker paths alike. `jailbee git pull`'s post-merge
cleanup destroy runs the identical guard (its `always` cleanup policy is
its own `--force`-equivalent bypass); the Qt dashboard (`jailbee gui`) shows
the same summary in its own dialog instead, because its destroy runs as a
detached, `--force`-appended background process that cannot answer a
terminal prompt.
- **GUI:** `jailbee ide <name>` (JetBrains; `--app webstorm` to override),
`jailbee chrome <name> [URL]`, `jailbee firefox <name> [URL]`,
`jailbee browser [<name>] [URL]` (whichever `browsers.default` names, or
the single enabled browser). All require the matching `jetbrains`/
`browsers.chrome`/`browsers.firefox` blocks enabled (usually in
`~/.config/jailbee/global.yaml`; the pre-1.3.0 top-level `chrome:` block
still works too, with a deprecation hint). One JetBrains IDE runs at a
time (shared profile); Chrome and Firefox are each per-container, from
their own cache pool slot (`jailbee pool ls`/`prune chrome-profile` /
`firefox-profile`; the old `jailbee chrome-pool ls`/`prune` spelling
still works for Chrome, deprecated). Firefox defaults to `source: image`
(built into the golden image) — Ubuntu's own Firefox is a snap, not
usefully mountable.
- **Other GUI apps:** anything registered under `apps:` (an AppImage, a
vendor binary, a wrapper script) launches with `jailbee apps run <name>
[<args>…] [--container <name>]`, or directly as `jailbee <name>` when the
entry sets `top_level: true`. `jailbee apps ls [<name>]` lists every app
the repo's config can launch — builtins plus `apps:` entries — and, given
a container, probes each one for `present`/`missing`.
- **Background commands:** `jailbee exec <name> -d -- <cmd>` (alias
`--detach`) runs any command detached — needed for a GUI app run by hand
(`jailbee exec smoke -d -- some-gui-tool`), useful for anything
long-running. It returns immediately; output goes to a log file inside
the container. Over a GUI-enabled remote SSH session add `--gui` for a GUI
app so it draws on the shared RDP display.
- **Cache pools:** `jailbee pool ls [NAME]` / `jailbee pool prune [NAME]` — any
cache configured with `pooled_caches`/`SharedCache.pool` (Gradle, Maven,
Chrome and Firefox by default) gets one private slot per container instead
of one cache shared by all of them, because those tools take a lock on the
cache directory that a shared mount serialised across containers. Omit
`NAME` for every pool.
`ls`'s footer total is deduplicated (hardlinked files counted once); the
per-slot sizes above it are not, and over-report when slots share files.
- **Snapshots:** `jailbee snapshot create <name> <tag>` / `restore <name> <tag>` /
`ls` / `delete` — cheap save/rollback of a container's state.
- **Optional mounts:** `jailbee mount <kind> <name>` / `jailbee unmount <kind> <name>` to
attach/detach an `optional_mounts` entry (e.g. `aws`) on a live container.
- **Housekeeping:** `jailbee disk-usage`, `jailbee prune` (stopped containers >30 days),
`jailbee doctor` (host + repo diagnostics), `jailbee apply` (re-push config — profiles,
ACL, /etc/hosts, dockerd proxy — after editing `.jailbee/config.yaml`; idempotent).
- **Advisory warnings:** `jailbee dismiss [KEYS…] [--all] [--clear]` marks a
repeating advisory read so it stops appearing on `jailbee ls`/`new`/`shell`.
Keys: `base-build` / `apply` (an owed action after an upgrade), `update`
(a newer JailBee release is on PyPI) and
`legacy-config-dir` / `legacy-chrome-block` / `legacy-credentials-block` /
`legacy-per-repo-map` / `legacy-pr-keys` (a deprecated config spelling —
`jailbee config migrate --apply` on the host moves most of them);
`KEY@scope` picks one of several files raising the same notice. With no
arguments it lists what applies and what has been dismissed. An owed action
returns when a later release adds a **new** reason for it — upgrading alone
does not; a dismissed `update` returns when a release newer than the
dismissed one appears; a deprecation stays dismissed until the config
changes.
`jailbee doctor` never respects a dismissal: it still reports both, marked
with the version they were dismissed at, so nothing is hidden from it.
Warnings that answer the command you just typed (what `jailbee config
validate` reports, `jailbee base build`'s `golden.python` line, a deprecated
alias) are deliberately not dismissible.
- **Update notice:** `jailbee ls`/`new`/`shell` print one stderr line when a
newer JailBee is on PyPI, naming the upgrade command for the install at
hand. The version is fetched by a detached background process at most once
a day, never on the command's own path, and `update_check: false` in
`~/.config/jailbee/global.yaml` (or `JAILBEE_NO_UPDATE_CHECK=1` for one
command) turns it off. An editable install is never advised.
- **Background jobs:** `jailbee job ls [--all-repos]` (in-flight/failed jobs with
phase, pid, age, error, log path), `jailbee job log <name> [--follow]` (print or
follow the worker log), `jailbee job clear [<name>] [--all]` (acknowledge a dead
job; refuses one whose worker is still alive). See "Background operations"
below.
## Shell completion
`jailbee setup` installs the completion scripts for both `jailbee` and `jb`
(once per machine; restart the shell after). Beyond commands/flags, TAB
dynamically completes:
- container names, on every command that takes one
- branch names, on `jailbee new` and `jailbee retarget`
- snapshot tags, on `jailbee snapshot restore`/`delete` (not `create` — that tag
doesn't exist yet)
- fixed values, for `--format`/`--layer`/`--attach`/`--user`
Needs `.jailbee/config.yaml` in the cwd, like every other command; elsewhere it
offers nothing rather than erroring.
## Background operations
`jailbee new`, `jailbee destroy`, `jailbee start` and `jailbee restart` block
until done (minutes for `new`; for the boot commands it is the autostart run
afterwards that takes the time). Add `--background`/`-b` to detach and get the
shell back immediately:
```bash
jailbee new feat/foo --background # returns at once; track with `jailbee ls`
jailbee restart feat-foo -b # reboot + autostart, detached
```
`jailbee ls` shows a **JOB** column with the live phase (`creating` → `cloning` →
`autostart:<stage>`, or `starting` → `autostart:<stage>` for a boot, or
`destroying` / `failed`) — `<stage>` is whichever autostart stage a
detached run is currently on; it clears when the container is ready.
A failed background job leaves the container intact for inspection (`jailbee shell`,
then destroy). `jailbee shell`/`tmux` on an in-flight container **wait** for it to
finish, then attach — for a create and a boot alike, the wait ends early at
`autostart`, when the container is up and the steps are visible in tmux. Make it
the default with `new.background: true` / `destroy.background: true` /
`boot.background: true` (one key for both `start` and `restart`) in config;
`--no-background` forces one-off foreground.
A background boot refuses to start while another job for that container is
still live: two workers would interleave their autostart steps.
`--attach shell`/`--attach tmux`, `--tmux`, `--shell` force foreground
(overriding `new.background`) and conflict with an explicit `--background`;
`--attach none` / `--no-attach` don't force foreground and combine fine with it.
A `failed` job is a database record, not a container state — the container
(if one exists) is left running untouched. `jailbee job ls` shows the recorded
error and worker log path; `jailbee job clear <name>` is how the record is
acknowledged (the dashboards expose the same action as "Clear failed job").
A failed *boot* record clears itself: the next `jailbee start`/`jailbee restart`
that completes supersedes that boot and drops the row. A failed create's record
does not — the container's setup never finished, so it stays until acknowledged.
### Autostart stages that keep running after the hand-off
`autostart`'s `on_create`/`on_start` can be written as **stages**, and a
stage marked `detach: true` — plus every stage after it — runs in a
background supervisor once the CLI would otherwise wait for it, so
`jailbee new`/`start`/`restart` hand you the session after the *last
blocking* stage rather than after every one. This applies whether or not
`--background`/`-b` was used: it's a property of the config (or the
`--wait`/`--no-wait` override below), independent of whether the whole
command ran in the foreground or was backgrounded.
- `jailbee autostart status <name>` — one row per step, grouped by stage,
for the run a detached supervisor is (or was) working through.
- `jailbee autostart cancel <name>` — SIGTERM the supervisor; it unwinds
the stage it's on (interrupts the running step, best-effort; detaches the
stage's mounts; restores the network) before marking the job failed with
the cancellation as its reason. Refuses once the worker is already gone
— `jailbee job clear <name>` is what drops that record.
- `--wait` / `--no-wait` on `new`/`start`/`restart` override `detach: true`
for one run: `--wait` runs every stage in the foreground (the pre-1.4
behaviour), `--no-wait` hands off after the first stage regardless of
what the config says. Mutually exclusive.
- `jailbee stop` refuses while a detached run is in flight (`--force`
cuts it off along with the confirmation); `jailbee restart` refuses too,
with **no** `--force` escape hatch — cancel the run first. `jailbee net`
only warns and proceeds: a detached stage's own network restore is
compare-and-swap, so a mode you set by hand while it's running stands.
- `jailbee destroy` does **not** guard against a detached run — destroying
mid-run tears the container down out from under its own supervisor.
`jailbee job log <name> [--follow]` prints a detached run's supervisor
output — there's no separate `jailbee autostart log`. See
[Configuration](../../config.md#detaching-a-run) for the full stage/chain
schema and [Security](../../security.md#autostart-and-the-network-exposure-window)
for the network-exposure details.
## Reviewing a pull request
```bash
jailbee new --pr 1234 # fetch PR #1234's head, create a container on it
jailbee shell <derived-name> # review/test
jailbee git push <derived-name> --pr --rebase # pull in commits the author pushed since
jailbee pr <derived-name> # publish your own commits (asks once: into PR #1234, or stacked)
jailbee destroy <derived-name> --force
```
Requires the `gh` CLI authenticated on the host (`gh auth login`). Fork PRs work
for *review*; `jailbee pr` refuses to publish to a fork PR's head. The PR number is
stored as `user.jailbee.pr` on the container.
Neither step needs `jailbee net loose`: `jailbee git push --pr` fetches the PR head on the
**host** and moves it in over the bridge, and `jailbee pr` publishes host-side too.
The first `jailbee pr` asks — an arrow-key menu — what to publish, and records
the answer: the container's commits **into** PR #1234's head
(`user.jailbee.pr_adopted`), or a **new PR based on** that head (a stacked PR,
see below). Off a TTY the choice must come from a flag: `--yes` for the first,
`--stacked` for the second.
`git push --pr` with no name on a TTY selects one running clone-mode PR
container (or uses the only eligible one). Name it explicitly in scripts.
The action flag is optional: without it the merge/rebase/plain choice follows
`push.default_action`, which is `ask` by default — a prompt on a TTY, an error
off one. Both dashboards carry it as **"Refresh from PR head"**, shown only on a
review container (a PR JailBee opened from the container's own branch has its
head downstream of the container, so refreshing could only be a no-op).
`jailbee new --pr` never touches your branches: the head is fetched into JailBee's own
`refs/jailbee/pr/<N>/head` and the container's clone is checked out at that exact
commit. So reviewing your own PR works with its branch checked out on the host
(git refuses to fetch into a checked-out branch), and a stale or diverging local
branch of the same name cannot leak into the container.
### A PR against the PR — `jailbee pr --stacked`
Reviewing often produces work of its own. `--stacked` publishes it as a PR
whose **base is the reviewed PR's head branch**, instead of pushing into that
PR:
```bash
jailbee pr <derived-name> --stacked --as fix/worktime-review
```
Five things to know when explaining it:
- **It needs a head branch of its own** — `--as`, or Claude's proposal
confirmed on a TTY. Publishing under the reviewed PR's head is refused
(exit 2): that would silently update the reviewed PR. `--stacked --no-ai`
hits this, because the proposed name then defaults to the container's
branch, which *is* that head.
- **The reviewed PR keeps `user.jailbee.pr`.** The stacked PR is recorded
separately (`user.jailbee.stacked_pr` / `stacked_pr_branch` /
`stacked_pr_author` / `stacked_pr_base`), so `jailbee ls`'s PR column and
`jailbee git push --pr` ("Refresh from PR head") go on tracking the parent
— which is what you want when the author pushes more commits.
- **It offers to retarget the container** onto the PR head
(`--retarget`/`--no-retarget`; the default asks on a TTY and otherwise
prints the command). Retargeting changes the recorded base, and status then
resolves that branch against the host's live local (or last-fetched tracking)
ref. A matching target is required to calculate status; without one the
comparison is unknown rather than falling back to the PR anchor. When the
parent merges, `jailbee git retarget <name> <base>` moves it on as usual.
- **Later runs need no flag.** They read the stacked labels, update that PR
silently, and `--open` prefers it over the parent.
- **Refused** on a fork PR (its head is not a branch in your origin, so it
cannot be a base — open the stacked PR in the fork instead), on a container
that already publishes to a PR's head (that head is fixed), and on one never
created from a PR (there `jailbee new <branch> <base>` is the way to base
work on another branch).
`jailbee new --pr` fetches two things: the PR head, and the PR's base branch into
`origin/<baseRefName>`. The fetched branch supplies a tracking fallback when
there is no local target; if a local base branch exists, status deliberately
uses it instead. Status line diffs are direct tree comparisons, not a GitHub
three-dot PR view; use `jailbee git diff --incoming` for the contribution-only
three-dot patch. `--no-fetch` skips both fetches.
## Publishing a PR — `jailbee pr`
The "I'm done, ship it" companion to `jailbee git pull`. `jailbee pr <name>` publishes the
container's branch to GitHub and opens (or updates) a PR:
```bash
jailbee pr feat-foo # first run: open a DRAFT PR; later runs: push new commits to it
jailbee pr feat-foo --ready # open (or flip) it ready-for-review
jailbee pr feat-foo --web # …and open it in the browser
```
Key point: it publishes **host-side** — JailBee fetches the container's branch to the
host, then fast-forward pushes it under the **host's** `gh` credentials. So,
unlike `jailbee git push --pr`, you do **not** need `jailbee net loose` on the container
for `jailbee pr`; you need `gh` authenticated on the host.
On a container created with `jailbee new --pr N`, `jailbee pr` does not open a second PR:
it asks once whether to push the container's commits to PR #N's head branch,
records the answer (`user.jailbee.pr_adopted`), and updates that PR from then on.
`--yes` skips the prompt and fork PRs are refused outright. Because the PR is
not JailBee's own, two extra guards apply on **every** run: `--force` asks again
before overwriting that head (`--yes` skips), and the interactive "regenerate
the description?" offer is suppressed, so the PR author's text is never replaced
unless you pass `--description`/`--title`/`--body`.
The same applies when the container was **not** made from a PR but its branch
already has one (`jailbee new <existing-branch>` on a branch you already opened a PR
for). `jailbee pr` checks GitHub for a PR on the container's branch before opening
anything and offers to push to it — `Push this container's commits to PR #77
instead of opening a new one? [Y/n]` — recording the same `pr` / `pr_branch` /
`pr_adopted` labels, and *not* `pr_author`, so the guards above stay on.
Declining exits without publishing; `--yes` skips the question. A closed/merged
PR or a fork PR falls through to opening a new PR (with a printed reason), and
`--as` skips the check entirely. Without it the AI-proposed head branch name
would publish the work under a new branch and open a duplicate PR.
That check is by **branch name** (`gh pr view <container branch>`), so it finds
nothing when the container's branch is named differently from the PR's head —
the usual case when the PR was opened from another branch. `--pr N` names the
PR outright:
```bash
jailbee pr feat-foo --pr 77 # push this container's commits to PR #77's head
```
It resolves PR N, refuses a closed/merged or fork one (an explicitly named PR
must not be silently replaced by a new one), asks the same confirmation, and
records the same `pr` / `pr_branch` / `pr_adopted` labels — so the hands-off
guards apply here too. Re-running with the same number is a no-op; a
*different* number asks before retargeting, which is also how a mistyped
number gets corrected. `jailbee submodule pr --pr N` does the same for one
submodule, resolved against the submodule's own repo and remote.
`--as` is rejected (exit 2) on **any** container that already has a PR — its
head is fixed, so a different branch name would leave the PR untouched.
`--pr` and `--as` together are a usage error (exit 2).
When `pr.ai_description` is on (the default) and the container has an agent with
a one-shot (`headless`) mode, a new PR's **title and body are written by that
agent**, and `pr.ai_branch` proposes a convention-following head branch name
(confirmed interactively). Which agent is `pr.agent`: `auto` (default) is the
repo's own — the autostart agent, Claude preferred, `claude-jb` when
`litellm.autostart` is on; name one (`codex`, `claude-jb`, ...) to pin it. A
pinned agent that is not enabled or has no `headless` command is reported and the
placeholder is used — never another agent. Opt out with `--no-ai`, or override per
field with `--title`/`--body`/`--as`. Updating an existing PR leaves the
description alone unless you pass `--description` (regenerate with the agent),
`--title`/`--body`, or accept the prompt — which is offered only for a PR JailBee
itself created.
`--force` force-pushes (with lease) a rebased/amended branch.
The generation reads the branch's commits and cumulative diff, plus
`.github/pull_request_template.md`, the spec or issue the branch implements, and
`CONTRIBUTING.md` / `CLAUDE.md` / `AGENTS.md`. It runs on `pr.model` (left unset:
`sonnet` for Claude and `claude-jb`, the agent's own default for any other;
an explicit `null` inherits the container's default model). A repo can state its
own PR conventions in `pr.prompt` — those instructions outrank JailBee's generic
guidance about the title and body.
It is explicitly told **not** to run the project's tests, build, linters or
installers, and to describe how the change was tested from the commits and the
CI config instead — the run has a fixed budget while a test suite's cost belongs
to the repository. `pr.timeout` (default 600 s) bounds the whole run;
on expiry `jailbee pr` warns and falls back to a placeholder title/body, which
you can replace later with `jailbee pr --description`. Raise the timeout for a
large tree, or when `pr.prompt` asks for slower work.
## The PR review outbox — `jailbee review`
A container's own `gh` is read-only by design (see "Reviewing a pull
request" above): an agent inside it that wants to post review comments,
reply to one, or rewrite a PR's description cannot call the GitHub API
directly. Instead it writes a JSON manifest into `~/.jailbee/pr-outbox/` —
the **container-side** contract, exact schema and worked examples live in
the **jailbee-pr-review** skill, not here. What matters on the host:
```bash
jailbee review ls # every running container's pending manifests
jailbee review show <name> # print every pending body in full (rendered on a terminal)
jailbee review apply <name> --dry-run # print the plan, publish nothing
jailbee review apply <name> # ask once, then publish to GitHub
jailbee review drop <name> # delete pending manifests unapplied
```
No `<name>` and a TTY picks the one container with something pending, or
prompts when several do. `apply` shows the whole plan — every comment, reply
and description body — before one confirmation (`-y` skips it; refused off a
TTY without `-y`). A manifest whose PR head moved since it was written is
held back (`--force` posts its line comments anyway, which GitHub then shows
as outdated). Applying deletes the manifest and appends a line to
`~/.jailbee/pr-outbox/applied.log`; re-running `apply` is a no-op for
anything already published, so nothing double-posts.
**`jailbee pr --no-outbox`** skips the outbox lookup entirely, including the
offer described below — use it when a stray or unwanted manifest should be
ignored for this run.
**Where the PR description comes from**, in order: an explicit
`--title`/`--body` you typed outrank everything; failing that, a pending
outbox description (a `pr: null` manifest, or one naming this PR) wins;
failing that, the repo's agent (`pr.agent`: by default the one it autostarts,
Claude preferred) generates one (unless `--no-ai`);
last resort is the placeholder text. **`--no-ai` does not disable the
outbox** — a manifest is text that already exists, not an AI run, so
`--no-ai` only turns off the *generation* step. On an update, a pending
outbox description also outranks `--description`: `jailbee pr -d` with a
manifest pending applies the manifest and never calls an agent; `--no-outbox`
is what forces the regeneration `-d` asks for.
One more consequence worth calling out: `pr.ai_branch: false`
normally keeps the head branch name as the container's own, but it no
longer suppresses a rename when the outbox manifest itself proposes a
branch — the manifest's proposal is confirmed like an AI one regardless of
that toggle. Turn it off expecting no renames and a manifest can still
trigger one.
After a successful `jailbee pr` (create or update), if the container still
has unapplied **non**-description actions (comments, replies), it offers to
post them: `Post N pending PR comment(s) now? [y/N]`. A pending
*description* is never included in that offer — it either just got consumed
above or is left for `jailbee review apply` to handle explicitly. Declining,
or running off a TTY, prints the count and the `jailbee review apply`
command that publishes them later.
`jailbee ls`'s PR column shows `✉N` for N manifests waiting (even before any
PR exists, since a container can hold a description ahead of `jailbee pr`
opening one) — same marker on the dashboard cards, which also gain an
"Apply N PR action(s)" entry. The ISSUES column right after PR shows the same
`✉N` for the container's issue outbox (see **jailbee-issue-management**),
with its own "Apply N issue action(s)" dashboard entry. `jailbee destroy`
warns about unapplied PR actions the same way it warns about an unpushed
commit, since destroying the container takes the outbox with it — and about
unapplied issue actions the same way.
See the **jailbee-pr-review** skill for the manifest format and how the
in-container agent is expected to use it — this section only covers
publishing what it already wrote.
## The issue outbox — `jailbee issue`
The same pattern for GitHub issues. An in-container agent stages issue
creates, edits, comments, label changes, closes and reopens as JSON manifests
in `~/.jailbee/issue-outbox/` — across the superproject and any declared
submodule, since each action names its own `repo` — and a human publishes
them from the host:
```bash
jailbee issue ls # pending manifests across this repo's containers
jailbee issue show feat-foo # every pending proposal in full, never truncated
jailbee issue apply feat-foo # one plan across all of them, one confirmation
jailbee issue drop feat-foo 001-triage.json
```
`apply` refuses the whole batch if any action's `expected` block (the issue
state the agent read) no longer matches GitHub, and checks again right before
it mutates anything. An action whose outcome could not be confirmed is
journaled `uncertain` and blocks its manifest until `jailbee issue resolve
NAME MANIFEST ACTION --applied --url URL` (it did land) or `--retry`. Pending
issue actions show in `jailbee ls`'s ISSUES column, as an "Apply N issue
action(s)" dashboard entry and in the pre-destroy warning. The manifest format
is the **jailbee-issue-management** skill's; flags are in
[`references/commands.md`](references/commands.md#issue-management-outbox).
## Browsing both outboxes — `jailbee outbox`
`jailbee outbox [CONTAINER]` opens one browser over a container's PR **and**
issue proposals (the Qt dashboard has a native window for it). The
scriptable forms address a proposal as `pr/<manifest>.json` or
`issue/<manifest>.json`:
```bash
jailbee outbox ls --all-repos
jailbee outbox show feat-foo pr/001-review.json
jailbee outbox drop feat-foo pr/001-review.json --action 0 --comment 1 # one inline comment
jailbee outbox apply feat-foo issue/001-triage.json
```
Selectors are zero-based, and `drop` can remove a whole manifest, one action
or one inline review comment. `apply` publishes one whole manifest through the
same gates as `review apply` / `issue apply`. Inspection is local, never calls
GitHub, and is read-only over remote SSH. Flags:
[`references/commands.md`](references/commands.md#unified-outbox).
## Publishing a submodule PR — `jailbee submodule pr`
The counterpart of `jailbee pr` for work done **inside a submodule**. A
submodule is its own GitHub repository, so it needs its own PR — `jailbee pr`
only ever publishes the superproject branch. One PR per run; the two commands
don't depend on each other.
```bash
jailbee submodule pr feat-foo # auto-target, draft PR
jailbee submodule pr feat-foo libs/foo # explicit submodule (path is top-relative)
jailbee submodule pr feat-foo --ready # mark ready for review
jailbee submodule pr feat-foo --open # just open it in the browser
```
Without a path, the submodule with commits ahead of its own base is targeted
automatically; several ahead lists them and asks you to name one (two
submodules are two repositories and two PRs). None ahead is reported as a
plain fact, not an error.
On a TTY, `jailbee submodule pr` is interactive: it asks which container even
when there is only one, offers a picker over every submodule instead of
erroring when several are ahead, and shows a plan block to confirm before
anything is transported or published — `--yes` skips that confirmation but
not the pickers. Naming NAME/PATH skips the corresponding picker. Off a TTY
none of this applies: the auto-targeting and several-ahead behaviour above
still runs and no exit code changes, but the several-ahead listing now
renders through the same code the picker uses, so it gains
`[dirty]`/`[gitlink stale]`/`[detached]` flags — a script grepping that
listing sees more than before.
The key thing to know: the signal is the submodule's **own** base anchor
(pinned when the container was created), not the superproject's gitlink diff
`jailbee ls` shows. So if you've committed inside the submodule but haven't
yet committed the gitlink bump in the superproject, `jailbee submodule pr`
still sees exactly the commits to publish — `jailbee ls`'s AHEAD column would
read zero for the same container. That gap is reported as information, never
an error.
Base and head branch names come from the submodule's **own** git data, not
the superproject's: base is `--base` > the submodule's `.gitmodules` entry >
its own `<remote>/HEAD` > `main`; head is `--as` > the agent's proposal > the
branch the commits came from. The chosen head is remembered per submodule
path, so re-running updates that PR instead of opening a second one. Note
that `--branch/-b` means something different here than in `jailbee pr`: it
selects which branch to read **from the submodule**, and is the escape hatch
for a detached submodule.
When the container also has a superproject PR, a successful run notes the
merge order as information only: merge the submodule PR first, so the
superproject PR's gitlink bump then points at a merged commit.
## Using `gh` from inside a container
`gh` is baked into every container. For it to authenticate, `github.enabled`
must be true in the global config and this repo's token must be set in
`~/.config/jailbee/repos/<container_prefix>.yaml` as `github.token`. With
`github.enabled`, `api.github.com:443` is on the strict allowlist, so `gh`
**reads** (`gh pr view`, `gh issue list`, `gh api`) work in strict mode.
**Writes don't**, by design: the token is read-only, so `gh` cannot comment,
edit or create anything, and a `git push` to GitHub has no credential either.
Stage GitHub writes in the outbox instead — PR comments, replies and
descriptions with **jailbee-pr-review**, issue actions with
**jailbee-issue-management** — and publish commits by asking the host operator
for `jailbee git pull` / `jailbee pr`.
## Inside a JailBee container
You may be reading this from **inside** a JailBee container — the repo clone lives at
`~/<container_prefix>` and there is no `jailbee` binary or Incus daemon on `PATH`. If
so, you **cannot** run `jailbee ...` commands here; they all operate on the host.
What you can still do from inside:
- Work in the repo clone, commit, run tests/builds — exactly as on a normal dev box.
- Edit `.jailbee/config.yaml` and any `install.d/` snippets in the clone. Those
changes only take effect when the **host operator** runs `jailbee apply` (live
profile/ACL/`/etc/hosts`/dockerd changes) or recreates the container (image /
install-time changes).
- For host-side bridge actions (`jailbee git pull`, `jailbee destroy`, `jailbee net loose`, …),
describe what you need and let the host operator run it — you can't from here.
## Checking what an agent preset resolved to
`jailbee config show` prints the merged effective config, including the
resolved `agents:` block — preset fields filled in whether or not the
repo's own config mentions them:
```bash
jailbee config show | less # look for the `agents:` block
```
That's the way to answer "what did enabling `codex`/`claude`/… actually
turn on" instead of re-deriving it from the preset source by hand.
Configuring a new agent, not just inspecting one already on, is the
**jailbee-repo-setup** skill's job.
## When to point elsewhere
- Writing (not publishing) PR review comments/replies/descriptions from
*inside* a container — the manifest format, the outbox contract, `gh`
read-path recipes → **jailbee-pr-review** skill.
- Staging GitHub issue creates, edits, comments, labels, closes or reopens
from *inside* a container → **jailbee-issue-management** skill.
- Changing what a container installs, its autostart steps, egress allowlist,
resources, or any `.jailbee/config.yaml` field → **jailbee-repo-setup** skill.
- First-time host setup (`jailbee init`, UFW/subuid/keyring, `jailbee base build`) →
the repo [`README.md`](../../../README.md) "Quick start".
- Exact flags / edge cases not on this page →
[`references/commands.md`](references/commands.md).