Skip to content

Internals

agedum compiles source into a throwaway directory and binds required files at their harness-visible targets under bubblewrap, so the compiled files appear at their expected paths for that process — and only that process. Compiled context stays in temporary staging; writable provider seeds and mountpoint preparation are deliberate persistent filesystem writes, described below.

The launch pipeline

flowchart TD
  a["load_source() + load_global_source()"] --> b["compile_claude / compile_kimi / compile_opencode / compile_cline / compile_reasonix / compile_aider / compile_pi / compile_codex<br/>→ Plan(binds, extra_args, safe_overrides)"]
  b --> c["assert_safe(): refuse git-tracked targets"]
  c --> d["bwrap --dev-bind / / --tmpfs shadow … --ro-bind src target … -- command extra_args"]
  d --> e["child runs, sees injected files"]
  e --> f["sweep stub mountpoints bwrap left behind"]

The main modules are:

  • sources.py — locates the project root and the project/global source files into a Source (root, agents_md, skills_dir).
  • harness.py — compile_claude / compile_kimi / compile_opencode / compile_cline / compile_reasonix / compile_aider / compile_pi / compile_codex render a Source pair into a Plan: a list of absolute (compiled-file → mount-target) binds, plus extra_args to append to the command, plus safe_overrides — targets to shadow with an empty tmpfs instead of binding content. Pi does not shadow source skills: it uses exact native exclusions in an untracked project .pi/settings.json, leaving source files and the shared Git index visible (see Pi).
  • launcher.py — assert_safe, build_bwrap_argv, and run_virtualfs validate, compose the bwrap argv, run the command, and clean up.
  • proxy.py — launch-bound localhost reverse proxies (the child reaches them at 127.0.0.1, shared into the bwrap namespace): FoldProxy folds Anthropic system messages, TranslateProxy translates Anthropic ↔ Chat Completions, ResponsesToChatProxy translates Responses ↔ Chat Completions, and FailoverProxy routes OpenCode model chains. Every local proxy requires its own runtime-only launch capability; loopback alone is not authorization.

Provider mode adds one more injection channel on top of the same pipeline: Launch.config_files — agedum-generated config files a harness needs on disk (reasonix's project-root reasonix.toml; pi's user-scope models.json / settings.json, deep-merged onto any existing file). The CLI writes each into the throwaway dir and appends it to plan.binds, so generated configs go through the same git-safety check and stub sweep as every other bind. A config file flagged writable (Kimi's custom-endpoint config.toml or Cline's providers.json) is atomically replaced at its real target, created owner-only before any secret bytes are written, and rejected if its parent or target is unsafe. It has no --ro-bind, so harness rewrites remain possible. Dry-run uses the same plan construction as runtime and computes prospective grants and read-only generated-config binds without creating a seed.

The compiled tree lives under a tempfile.mkdtemp() directory that is removed when the command exits.

To see the Plan for a given source without launching — the exact mount targets the harness will read — run --dry-run: agedum <name> --dry-run (provider mode) or agedum --wrapper <harness> --dry-run -- <cmd> (wrapper mode). It compiles the source, prints the bind targets and any appended args, and stops before the bwrap step.

The mount namespace

The bwrap argv starts by mirroring the whole real filesystem read-write into the namespace, then read-only-binds each compiled file over its target:

bwrap --dev-bind / / --ro-bind <compiled> <target> … -- <command> <extra_args>

Because the binds use absolute targets, the same mechanism places project-scope files inside the tree (./CLAUDE.md) and global-scope files under the user config dir (~/.claude/...). A --ro-bind masks any pre-existing file or directory at the target for the duration of the run, and the mask is visible only inside this namespace — other processes, and your shell after the command exits, see the original tree.

Directory binds are overlaid one level deep, not masked wholesale. The only directory binds agedum emits are skill trees, and binding the whole compiled dir over ~/.config/opencode/skills/ (or ~/.claude/skills/, ~/.kimi-code/skills/) would hide any hand-authored skill already living there. So build_bwrap_argv expands a directory bind into one --ro-bind per child — <compiled>/<name> over <target>/<name> — leaving unrelated siblings in the real target dir visible. A shipped skill still wins over a same-named on-disk one (its child bind overlays that subdir); only folders agedum does not ship survive. File binds (CLAUDE.md / AGENTS.md) pass through unchanged.

This is why agedum is harness-agnostic at the launch layer: every agent CLI ultimately just reads files, and the namespace makes the compiled files be those files.

Safety

Two rules, both validated empirically and not to be regressed:

No git-tracked targets

The namespace shares the project's real, shared .git directory — it is not masked. So a git add / git commit run inside the namespace writes to the real repository. If agedum overlaid a git-tracked file (say a real CLAUDE.md you keep in the repo), injected content could be committed by accident.

assert_safe therefore refuses to inject over any git-tracked path. Targets must be untracked; keep them gitignored as an operator prerequisite. Git ownership is queried from each target's actual enclosing worktree, independently of instruction-source discovery. This covers nested projects and global targets inside other repositories, and unexpected Git errors refuse the launch. In practice: list CLAUDE.md, .claude/, .kimi-code/, .opencode/, .cline/, .reasonix/, .pi/, and .codex/ in your .gitignore.

The check runs over the effective, per-child binds — the exact paths the namespace will mount. A tracked but unrelated sibling inside a skills target dir (say a hand-authored skill you deliberately version under .claude/skills/) does not block the launch, because the per-child overlay never masks it; only a path agedum would actually bind over must be untracked. safe_overrides receive the same check: hiding a tracked file can stage a deletion in the shared index even when the shadow is read-only.

Stub sweeping

To bind a file at a path that does not yet exist, bwrap creates the mountpoint on the real filesystem first. After the namespace exits, those mountpoints remain as empty stubs (a 0-byte file, or an empty directory) — the injected content never leaks, but the empty placeholder can.

run_virtualfs records which candidate paths existed before the run, and after the command sweeps the ones it created — deepest first, and only if still empty. Candidates are each target and its immediate parent, taken over the dir-level binds (e.g. the .claude dir created to hold .claude/skills), the per-child overlay targets (e.g. .claude/skills/<name> — the empty stub left when agedum ships a skill the target dir did not already have), and the safe_overrides tmpfs shadows (bwrap creates their mountpoints the same way). Anything that pre-existed — including a user's ~/.config/opencode/skills/ that already held skills — is left alone. Directories prepared before this snapshot (exact injection parents and harness state dirs under a sandbox) can remain, as can writable provider seeds and files the harness creates. Stub sweeping is not a guarantee of a clean or untouched working tree.

Adding a harness

A new harness is a single compiler function compile_<harness>(project, global_, dest) -> Plan. It renders the source however that harness expects and returns binds and/or extra_args. Register it in the CLI's _COMPILERS table under its --wrapper <harness> name. The harness need not fit the pure path-discovery shape: the aider harness injects instructions at read-only bound cache paths named by --read flags, and skips skills entirely — both expressed through the same Plan(binds, extra_args) return. The launcher and safety rules are shared, so a new harness inherits the namespace, git-safety, and cleanup for free.