Opinionated tool for jailing AI Coding agents.
repoman
A safety harness for AI-assisted development. Per-project Incus containers + opinionated NFS backup. Current version: v0.8.0.
repoman runs each of your projects inside its own Incus (LXC) container with the repo bind-mounted in, so AI coding agents can work with broad permissions inside the container while your host OS, your other projects, and the source repo on disk stay protected. See BLURB.md for the marketing pitch and VISION.md for the design rationale.
Why
While AI coding agents increase developer productivity, they often require "Permissions" approvals which halt the progress waiting for user input, and are also prone to causing unintended changes to things you didn't ask them to touch. repoman makes the blast radius of any mistake equal to one container:
- Host OS sealed off — the agent can mangle the container's filesystem; the host's
/etc,/usr, kernel modules, and services are out of reach. - Projects isolated from each other — each project is its own container; an agent in project A cannot see project B.
- Source code preserved — your repo lives on the host (
~/repos/<name>) and is exposed to the container via a uid-shifted bind mount.repoman removedeletes the container, never the repo.repoman syncmirrors all repos to NFS for belt-and-suspenders recovery. - Authenticated agent state shared safely — vendor profiles let you bind-mount things like
~/.claudeinto every container so you don't re-authenticate per project, without exposing SSH keys or shell config. - Skip Permissions - you can run agents in dangerous modes and bypass permisisons with less risk of consequences.
Build
You will need a Reef Language compiler reefc >= 0.9.3.
make
Produces ./build/repoman.
Test
make test
Or per-suite:
for t in tests/test_*.reef; do
echo "== $t =="
reefc run "$t" || exit 1
done
Install
sudo make install # installs to /usr/local/bin/repoman
# vendor profiles to /usr/local/share/repoman/profiles/
Or install from a release bundle (no reefc/make required):
tar -xzf repoman-<version>-linux-amd64.tar.gz
cd repoman-<version>-linux-amd64
sudo ./install.sh # honors PREFIX (default /usr/local)
Release bundles are built and published automatically by Isurus CI
on each vX.Y.Z tag (build + test run on every push and PR).
Quickstart
repoman setup # one-time host bootstrap (idempotent)
repoman profile install --all # install vendor profiles
repoman new my-project # bind ~/repos/my-project into a new container
repoman shell my-project # drop into it
repoman sync # rsync all repos to NFS
repoman remove my-project # destroy container; repo on host is untouched
Subcommands
repoman now generates its --help output from sys.optparse. Top-level:
$ repoman --help
repoman - Per-project Incus containers + opinionated NFS/ZFS backup
Usage: repoman [options] <command> [command options] [arguments]
Options:
-h, --help Show this help
-V, --version Show version
Commands:
new Create a new container + repo bind
sync rsync local repos → NFS backup
list List registered projects
help Show this help
status Show registered projects' state
remove Remove a project: delete its container and registry entry
rename Rename a project across container, registry, and repo dir
shell Open a login shell inside a project's container
setup First-time host bootstrap: incus project, registry, host LAN IP detection
profile Manage container profiles
Global options go before the command. Command options go after it.
Every subcommand also has its own --help (e.g. repoman new --help):
$ repoman new --help
new - Create a new container + repo bind
Usage: repoman new [options] <name>
Options:
-h, --help Show this help
--repo <string> repo dirname (defaults to <name>)
--image <string> container image (overrides default)
-v, --verbose show subprocess output (incus probes)
-q, --quiet force quiet mode even if config sets verbose
Global options go before the command. Command options go after it.
$ repoman sync --help
sync - rsync local repos → NFS backup
Usage: repoman sync [options] [arguments]
Options:
-h, --help Show this help
--no-delete additive only — no deletions on the destination
--dry-run preview changes without writing
-v, --verbose show subprocess output (NFS probes)
-q, --quiet force quiet mode even if config sets verbose
Global options go before the command. Command options go after it.
$ repoman list --help
list - List registered projects
Usage: repoman list [options]
Options:
-h, --help Show this help
Global options go before the command. Command options go after it.
$ repoman status --help
status - Show registered projects' state
Usage: repoman status [options] [arguments]
Options:
-h, --help Show this help
Global options go before the command. Command options go after it.
$ repoman remove --help
remove - Remove a project: delete its container and registry entry
Usage: repoman remove [options] <name>
Options:
-h, --help Show this help
-y, --yes skip the confirmation prompt
--keep-incus leave the incus container; only remove from registry
Global options go before the command. Command options go after it.
The source repo on host is never deleted by remove.
$ repoman rename --help
rename - Rename a project across container, registry, and repo dir
Usage: repoman rename [options] <old> <new>
Options:
-h, --help Show this help
-y, --yes skip confirmation prompt
--migrate-claude-history also move the Claude history slug dir
Global options go before the command. Command options go after it.
rename is idempotent (safe to re-run after a partial rename). A project
whose repo dirname is deliberately decoupled from its name has only its
identity renamed; the repo directory is left untouched.
$ repoman shell --help
shell - Open a login shell inside a project's container
Usage: repoman shell [options] <name>
Options:
-h, --help Show this help
--cwd <string> override the working directory inside the container
Global options go before the command. Command options go after it.
$ repoman setup --help
setup - First-time host bootstrap: incus project, registry, host LAN IP detection
Usage: repoman setup [options]
Options:
-h, --help Show this help
--non-interactive accept all defaults without prompting
Global options go before the command. Command options go after it.
$ repoman profile --help
profile - Manage container profiles
Usage: repoman profile [options] <command> [command options] [arguments]
Options:
-h, --help Show this help
Commands:
list List available and installed profiles
install Install a profile into incus
diff Diff a profile against the installed copy
remove Remove a profile from incus
show Show a rendered profile
Global options go before the command. Command options go after it.
repoman profile manages Incus profiles from the vendor library
(/usr/local/share/repoman/profiles/) and user overrides
(~/.config/repoman/profiles.d/); see Profile library
below for the individual profile subcommands' options.
--version / -V prints the version string to stdout.
Profile library
repoman ships a vendor profile library at /usr/local/share/repoman/profiles/:
claude-share— bind~/.claude(auth, history, plugins) and the host's~/.local/share/claudeversions tree (read-only) so containers share the host's Claude CLI. Oneclaude logincovers every project. A launcher at~/.local/bin/claudealways runs the newest installed version, so a host-sideclaude updatereaches running containers with no restart —claudeis upgraded once, on the host. In-container self-update is disabled (DISABLE_AUTOUPDATER=1) andclaude --upgradeis intercepted with a hint, since the shared install is read-only. Also setssecurity.nesting=true, required by Claude Code's bundled Bun/JavaScriptCore for interactive mode — without it the REPL crashes during GC thread suspension.llm-share— bind/usr/local/bin/ollamaand~/.ollama, setOLLAMA_HOST=http://<host-lan-ip>:11434so containers reach the host's ollama daemon over LAN.dotfiles— bind~/.gitconfigand~/.hgrc(extend with your own).
Manage with repoman profile:
repoman profile list
repoman profile install <name>
repoman profile install --all
repoman profile diff <name> # show drift between file and incus state
repoman profile show <name> # print rendered YAML
repoman profile remove <name> # remove from incus (file untouched)
$ repoman profile install --help
install - Install a profile into incus
Usage: repoman profile install [options] [arguments]
Options:
-h, --help Show this help
--all install every known profile
Global options go before the command. Command options go after it.
--all is a real optparse flag now, accepted anywhere in install's
arguments (repoman profile install --all and repoman profile install <name> --all both work) — previously it was matched only as the literal
first argument.
Customizing profiles
To override a vendor profile, drop a copy into your user dir and edit:
cp /usr/local/share/repoman/profiles/dotfiles.yml ~/.config/repoman/profiles.d/
$EDITOR ~/.config/repoman/profiles.d/dotfiles.yml
repoman profile install dotfiles # applies the user version (shadows vendor)
User profiles in ~/.config/repoman/profiles.d/ always win over vendor profiles of the same name. repoman profile list shows the active source for each.
Output and logging
By default repoman runs quietly — only its ==> ... markers and any errors reach the terminal. Subprocess probes (e.g. incus project show, findmnt) have their output suppressed. The actual user-facing operations (container launch, rsync) pass their stdio through.
Run with --verbose (-v) to see suppressed probe output for troubleshooting. Pass --quiet (-q) to force quiet if your config defaults to verbose.
Every invocation writes a log file at <logdir>/<project>-<verb>.log, truncated each run. So if repoman new veemarker fails, the log lives at ~/.local/state/repoman/veemarker-new.log until the next run with the same name+verb. Whole-tree sync uses all-sync.log.
Conventions
repoman is opinionated. Most knobs below are overridable (see Configuration), but the defaults are the path most users will take, and a few choices are baked into the source.
Defaults you can change
- Repo tree at
~/repos/<name>. Each project is one directory directly under~/repos. The container's bind-mount points there, andrepoman new <name>expects~/repos/<name>to already exist (use--repo <dirname>if the container name should differ from the dirname). Override globally viadefaults.repos_root. - NFS backup at
/nfs/repos/.repoman syncrsyncs~/repos/→/nfs/repos/. Override viadefaults.backup_root. - Incus project
repoman. All containersrepomancreates live in their own Incus project namespace so they don't intermix with hand-launched containers. Override viadefaults.incus_project. - Image
images:ubuntu/26.04/cloud. Override per-container with--imageor globally viadefaults.default_image. - Profile chain
["default", "claude-share"]. Override globally viadefaults.profilesor per-project inrepos.d/<name>.toml.
Hard-coded assumptions
These are not configurable without editing source. If your environment differs, repoman won't fit.
- Incus, not Docker or raw LXC. Profiles, projects, idmapped mounts, and
incus execare all assumed throughout. - One container per repo, named after the repo. No multi-container projects, no shared-container-across-repos.
- Host LAN bridge is
br0.repoman setupdetectshost.lan_ipby readingip -4 addr show br0. Profiles that template${HOST_LAN_IP}(notablyllm-share'sOLLAMA_HOST) won't get a useful value on hosts with a different bridge name. - NFS backup must be
nfs4and autofs-mountable.repoman syncpreflights the backup target in three steps:stat(to trigger autofs),mountpoint -q, thenfindmnt -t nfs4. A bare directory, an NFSv3 mount, an SMB share, or a manually-mountednfs(notnfs4) export will all fail the check even if technically writable. The expected setup is anautofs/etc/auto.nfsmap that mounts the NFS server on first access. - UID-shifted bind mounts (
shift=true) for every project disk. Host uid 1000 maps cleanly into the container as the cloud image's default user (Ubuntu cloud →ubuntu, uid 1000). Hosts where you work as a uid other than 1000 will see ownership mismatches. - Logs are truncated per run.
<logdir>/<project>-<verb>.logis overwritten each invocation — there's no rotation or history. - VCS-agnostic by way of bind-mount.
repomandoesn't introspect git or hg; thedotfilesvendor profile just ships~/.gitconfigand~/.hgrcinto the container so whichever VCS your repo uses works in-place.
Configuration
Central registry: ~/.config/repoman/repoman.toml (managed; do not edit while repoman is running).
Tool-level settings under [repoman]:
| Field | Values | Default | Effect |
|---|---|---|---|
output |
"quiet" | "verbose" |
"quiet" |
Default output mode (CLI flags override). |
Host facts under [host] (schema 3, added in v0.4):
| Field | Default | Effect |
|---|---|---|
lan_ip |
(auto-detected from br0) |
Substituted into profiles as ${HOST_LAN_IP} — used by llm-share to point containers at the host's ollama daemon. |
Defaults under [defaults]:
| Field | Default | Effect |
|---|---|---|
repos_root |
~/repos |
Local NVMe canonical location for working copies. |
backup_root |
/nfs/repos |
NFS-mounted backup destination. |
logdir |
~/.local/state/repoman |
Per-invocation log files. |
incus_project |
repoman |
Incus project containers are created in. |
default_image |
images:ubuntu/26.04/cloud |
Image used when no --image flag and no override. |
profiles |
["default", "claude-share"] |
Profiles applied when creating containers. |
Per-project overrides: ~/.config/repoman/repos.d/<container-name>.toml:
[container]
image = "images:debian/12/cloud"
profiles = ["default", "claude-share", "node-dev"]
[[mount]]
source = "~/.npm"
path = "/home/ctusa/.npm"
[env]
NODE_ENV = "development"
Smoke test (requires Incus + NFS)
# In an existing repo dir under ~/repos:
repoman new test-foo
repoman sync test-foo --dry-run
repoman remove test-foo
License
MIT License. (C)opyright 2026, Leafscale, LLC. See the LICENSE file for full terms.