|

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
Bookmarks
@
99f652f00b7e Bump version to 0.7.0
Chris Tusa <chris.tusa@leafscale.com> 10 days ago

repoman

A safety harness for AI-assisted development. Per-project Incus containers + opinionated NFS backup. Current version: v0.7.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.6.0.

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

Subcommand Description
setup [--non-interactive] First-time host bootstrap: create Incus project repoman, detect host LAN IP, write initial registry. Idempotent.
profile {list,install,diff,remove,show} Manage Incus profiles from the vendor library (/usr/local/share/repoman/profiles/) and user overrides (~/.config/repoman/profiles.d/).
new <name> [--repo <dirname>] [--image <image>] Launch a container in the repoman Incus project and bind-mount ~/repos/<dirname> with shift=true. Validates profiles are installed before creating.
list Print a table of all registered projects (read-only).
status [name] Show project state from registry + live incus query.
shell <name> [--cwd <path>] Open a bash login shell inside the project's container, in the repo's directory by default.
sync [name] [--no-delete] [--dry-run] Mirror local repos to NFS backup via rsync. NFS auto-mount aware.
`remove [--yes -y] [--keep-incus]`
--help / -h / help Show usage and subcommand list.
--version / -V Print version string.

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)

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 69%
Reef 28%
Shell 2%
YAML 1%
Makefile 0%
plaintext 0%
Activity
29 changesets
Updated 7 days ago