|

Opinionated tool for jailing AI Coding agents.

Clone
hg clone ssh://hg@leafscale.isurus.dev:2222/leafscale/repoman hg clone https://leafscale.isurus.dev/leafscale/repoman
Branches
default
Tags
v0.6.0 v0.6.1 v0.7.0 v0.7.1 v0.8.0
Bookmarks
@
da07af22a2cd ci: pull the toolchain from registry.sharkos.one, which has the image
Chris Tusa <chris.tusa@leafscale.com> 42 days ago

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 remove deletes the container, never the repo. repoman sync mirrors all repos to NFS for belt-and-suspenders recovery.
  • Authenticated agent state shared safely — vendor profiles let you bind-mount things like ~/.claude into 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/claude versions tree (read-only) so containers share the host's Claude CLI. One claude login covers every project. A launcher at ~/.local/bin/claude always runs the newest installed version, so a host-side claude update reaches running containers with no restart — claude is upgraded once, on the host. In-container self-update is disabled (DISABLE_AUTOUPDATER=1) and claude --upgrade is intercepted with a hint, since the shared install is read-only. Also sets security.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/ollama and ~/.ollama, set OLLAMA_HOST=http://<host-lan-ip>:11434 so containers reach the host's ollama daemon over LAN.
  • dotfiles — bind ~/.gitconfig and ~/.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, and repoman new <name> expects ~/repos/<name> to already exist (use --repo <dirname> if the container name should differ from the dirname). Override globally via defaults.repos_root.
  • NFS backup at /nfs/repos/. repoman sync rsyncs ~/repos/ → /nfs/repos/. Override via defaults.backup_root.
  • Incus project repoman. All containers repoman creates live in their own Incus project namespace so they don't intermix with hand-launched containers. Override via defaults.incus_project.
  • Image images:ubuntu/26.04/cloud. Override per-container with --image or globally via defaults.default_image.
  • Profile chain ["default", "claude-share"]. Override globally via defaults.profiles or per-project in repos.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 exec are 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 setup detects host.lan_ip by reading ip -4 addr show br0. Profiles that template ${HOST_LAN_IP} (notably llm-share's OLLAMA_HOST) won't get a useful value on hosts with a different bridge name.
  • NFS backup must be nfs4 and autofs-mountable. repoman sync preflights the backup target in three steps: stat (to trigger autofs), mountpoint -q, then findmnt -t nfs4. A bare directory, an NFSv3 mount, an SMB share, or a manually-mounted nfs (not nfs4) export will all fail the check even if technically writable. The expected setup is an autofs /etc/auto.nfs map 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>.log is overwritten each invocation — there's no rotation or history.
  • VCS-agnostic by way of bind-mount. repoman doesn't introspect git or hg; the dotfiles vendor profile just ships ~/.gitconfig and ~/.hgrc into 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.

License
MIT
Languages
Markdown 71%
Reef 26%
Shell 2%
YAML 1%
Makefile 0%
plaintext 0%
Activity
57 changesets
Updated 42 days ago