omh 0.2.1

Launch any coding harness, in a sandbox, with your setup already there.
# Architecture

How omh is put together: what lives where on disk, how images are built, and how
the runtime backend is kept swappable.

## On-disk layout

```
~/.omh/
  profile/                    layer 1 — personal, every project
  adapters/*.toml             one file per harness (data, not code)
  editors/*.toml              one file per editor
  base/                       the curated base set, versioned
  creds/<harness>/<account>/  captured logins, one directory per account
  worktrees/<repo>/<session>/ the agent's working directory
  keys/<repo>/                per-repo ed25519 keypair
  run/<session>/<harness>/    staged profile, regenerated per launch
  sessions.json               session → container, branch, port

<repo>/.omh/
  profile/                    layer 2 — project, COMMITTED, shared with the team
  local/                      layer 3 — project, GITIGNORED, yours alone
```

Two details that are load-bearing rather than incidental:

- **Staging is keyed by session *and* harness.** Without the harness in that
  path, launching a second harness overwrites the config the first one has
  mounted, live.
- **Worktrees live outside the repo.** Nested, your IDE would index every
  session's full copy of the codebase.

Merge rules are in [Configuration](../configuration.md#the-three-layers).

## Images

Two layers: a base every session shares, and a thin per-harness layer that runs
the adapter's `install` command.

```dockerfile
# omh/base
FROM node:22-bookworm-slim
RUN apt-get install -y … git ripgrep dtach sudo curl jq socat
RUN usermod -l agent -d /home/agent -m node        # node:slim holds UID 1000
RUN test "$(id -u agent)" = "1000"                 # assert, do not assume
RUN mkdir -p /work /omh/sock /omh/cache /omh/layers
USER agent
```

```dockerfile
# omh/<harness>
FROM omh/base:<recipe-digest>
USER root
RUN <adapter.install>
USER agent
```

**The base satisfies the `sbx` kit contract on purpose** — `agent` at UID 1000,
passwordless sudo, `/home/agent`, proxy env forwarding. One image works on
either backend, and an sbx kit becomes a two-line file rather than a port.

Three properties, each with a test:

- **The contract is asserted at build time**, not assumed. If a future base image
  moves UID 1000, the build fails there instead of failing mysteriously inside a
  sandbox weeks later.
- **Images end unprivileged.** Installing needs root; running must not have it.
  An image that ends as root hands the agent the sandbox's own escape hatch.
- **Dockerfiles arrive on stdin** with an empty build context, so nothing is
  written to disk and no context is uploaded.

### Tags are recipe digests, never `:latest`

A mutable `:latest` on the base meant `ensure` skipped rebuilding it, so base
recipe changes had **never** shipped — adding `socat` silently did nothing.
Harness layers now pin an exact base digest, so a recipe change actually
propagates.

`init` builds these, because init is not finished until `omh <harness>` works.
About 30 seconds on first run, cached after.

### A plan must be runnable, not merely well-formed

The per-project network a plan names has to be *created*, too. That gap made
every real launch die at `network omh-<repo> not found` while every unit test
passed — the archetypal case for [`omh doctor`](../troubleshooting.md).

### Verified end to end

Inside a real container, with the real mounts:

```
user:   agent uid=1000    home: /home/agent    cwd: /work
tools:  claude=ok dtach=ok git=ok rg=ok node=ok
  rules      # Global rules              (layers 1+2 concatenated)
  skills     graphify project-only       (unioned across layers)
  mcp        codegraph, filesystem, github, linear, omh-memory, sentry
  subagents  explorer.md
  hooks      PostToolUse, Stop           (rendered to claude settings.json)
  version    2.1.222 (Claude Code)
```

Every capability class arrives, merged across three layers, in the harness's own
format. That is the thesis demonstrated rather than argued.

## Runtime backends

Isolation is not ours to build. It is also not one vendor's to own.

The backend sits behind a narrow trait. `Plan` is already a pure description — a
mount list, env, and argv — so a backend is just a translation of that into one
process invocation.

```
        Plan  (pure: mounts, env, argv, workdir)
    ┌─────┴─────┐
    ▼           ▼
  Docker       Sbx
  shared       microVM: own kernel, own dockerd,
  kernel       egress policy, keychain-backed
               credential injection
```

Selection is `runtime = "auto" | "docker" | "sbx"` in `policy.toml`. `auto`
prefers `sbx` when present.

### Why not simply adopt `sbx`

It is better on every security axis — hypervisor isolation instead of a shared
kernel, and secrets injected at the egress proxy so **a compromised agent never
holds its own token.** omh currently mounts a credential volume, which means the
agent *can* read and exfiltrate it. That is a real flaw and `sbx`'s model is the
fix.

But a distribution whose opinion cannot be escaped is not trustworthy. Backends
stay plural.

### Is `sbx` harness-agnostic?

Yes — verified, not assumed. Docker's own docs
[demonstrate building a kit for Amp](https://docs.docker.com/ai/sandboxes/customize/build-an-agent/),
a third-party agent they do not ship. The four named agents are shipped *kits*,
not a constraint. A kit is barely more than an omh adapter:

```yaml
entrypoint:
  run: [amp, --dangerously-allow-all]
```

Base image requirements are four: a non-root `agent` user at UID 1000,
passwordless sudo, `/home/agent/` as home, and HTTP proxy env forwarding. omh's
`GUEST_HOME` is already `/home/agent`.

**An omh adapter can compile to an sbx kit.** Agnosticism expressed once.

### The seam

From the sbx FAQ:

> "Sandboxes don't import your complete user-level agent configuration. Hooks,
> settings, and other files under directories such as `~/.claude` remain on the
> host."

Docker drew its boundary exactly where omh's product begins. They isolate; they
deliberately do not carry your setup in. The two **compose**: `sbx` provides the
microVM, omh gets your profile inside it.

### Declared capabilities, and honest unknowns

Backends differ in ways that break a naive plan, so each **declares** what it can
do rather than failing mysteriously.

| Capability | Docker | `sbx` | Consequence if absent |
|---|---|---|---|
| bind-mount a single **file** | yes | **unknown** | staging must mount dirs + symlink instead |
| choose the **guest path** | yes | **no** — workspaces mount at the host path | the `/work` convention breaks |
| SSH attach for IDE | yes (sshd in image) | **unknown** | [editors]../editors.md need another mechanism |

The unknowns are not hand-waved. A `Plan` is validated against the selected
backend's declared capabilities and fails **loudly** if it needs something the
backend lacks. Running that validation against `sbx`'s conservative capabilities
today is instructive — **every single mount omh makes is rejected**:

```
Error: the selected runtime cannot honour this plan:
  /work would have to mount at its host path /Users/…/worktrees/s01
  /home/agent/.claude/skills would have to mount at its host path …
  /home/agent/.mcp.json is a single-file mount
  …
```

That is the real blast radius. If `sbx` genuinely forces host-path mounts, the
staging model needs rework rather than a tweak: staged content would have to be
written into the workspace and symlinked from inside, not mounted onto chosen
guest paths.

Better to know that from a failing validation than from an agent that starts
fine and cannot see its own profile.

Resolving it is a one-afternoon spike — build an opencode kit, try a single-file
mount, try attaching an IDE — and it gates whether `sbx` becomes the default or
stays opt-in hardening. See [roadmap](roadmap.md).