Accounts
$ omh auth claude --name personal$ omh auth claude --name work$ omh set account work # which one this project uses$ omh new claudeAn account is a captured snapshot of a harness’s own credential files.
omh auth runs the harness’s real login flow and keeps whatever it wrote.
Keyed by harness, not by provider
Section titled “Keyed by harness, not by provider”A harness is what can actually be captured. Two harnesses talking to the same provider still each need their own login, and there is no useful sense in which they share one.
Which account a session uses is a project-level setting, resolved through the usual two layers — because that is how it genuinely varies. This repo is work; that one is personal.
One name usually fits every harness. When it does not — accounts are captured
per harness, so a name one has may be one another lacks — name an account for
one harness with <harness>:<name>:
$ omh set account mineomh: `mine` is captured for claude$ omh new codexomh: no account `mine` for codex captured: work omh set account codex:<name> one for codex alone$ omh set account codex:workomh: `work` is captured for codex$ omh info --reposettings account mine ← shared account codex:work ← sharedaccount stays one string until a harness differs, then becomes a table whose
default is the name for every other harness:
[account]default = "mine"codex = "work"A harness’s own entry wins at whatever layer it is set. A plain
omh set account <name> sets default; omh unset account codex takes back
codex’s entry, and the table goes back to one string once no harness differs.
~/.omh/creds/<harness>/<account>/Mount the directory, never the token file
Section titled “Mount the directory, never the token file”creds = ["$HOME/.claude/", "$HOME/.claude.json"] # trailing slash = directorytoken = ["$HOME/.claude/.credentials.json"] # what proves a loginYou cannot rename() over a bind-mounted file. It is a mount point, so the
kernel returns EBUSY:
mv: cannot move '.tmp1' to '.credentials.json': Device or resource busyEvery tool saves a token by writing a temp file and renaming over it. Mounting the token file therefore means the login succeeds and silently never persists — you log in, it says success, and the next launch is logged out. Mounting the enclosing directory makes that rename an ordinary operation inside it.
That collides with capability mounts living in the same directory, because
Docker refuses to create a mountpoint inside a bind-mounted host directory
(“is outside of rootfs”). omh auth therefore prepares those mountpoints on the
host first: .claude/skills, .claude/commands, .claude/agents,
.claude/settings.json.
Docker also creates any missing mount parent as root, which leaves the agent
unable to write beside its own config — transcripts fail with EACCES and
atomic writes have nowhere to put their temp file. The harness image therefore
creates and owns every directory it will mount into, derived from the adapter.
None of that is guessable from documentation. All of it came from running the thing and reading the error.
token is what proves a login
Section titled “token is what proves a login”A harness fills its config directory just by starting: Claude Code writes
statsig/, projects/ and todos/ on boot. Inferring a login from “does this
directory contain anything” therefore reports success for a session with no
token at all — which is exactly the bug it replaced.
So the adapter names the file, and is_captured is defined as
unfilled(...).is_empty(), so the two answers can never drift apart.
Placeholders must be parseable, not merely present. prepare writes {}
into JSON files, because an empty file is valid TOML and valid YAML but not
valid JSON, and a harness that parses its config on startup refuses to run:
Claude configuration file at /home/agent/.claude.json is corrupted:JSON Parse error: Unexpected EOFA placeholder is recognised as one, so it never counts as a login.
A login that cannot finish in a sandbox
Section titled “A login that cannot finish in a sandbox”Codex’s default sign-in opens a browser and waits for the redirect on
127.0.0.1:1455 — inside the container, where the host’s browser never
arrives. Two ways round it:
-
Device code. In
omh auth codex, choose Sign in with Device Code: Codex prints a URL and a one-time code to enter in any browser, so no redirect has to reach the sandbox. -
Import. If you are already logged in on this machine, copy that login:
Terminal window $ omh auth codex --name work --import`work` captured for codexcopied from /Users/you/.codex/auth.json
--import copies the adapter’s token files and nothing else — not the config
directory around them, which also holds your config, skills and history, and
omh stages those separately. Every file is read before anything is written;
each is staged at 0600 and renamed into place only once all are staged, so a
file omh cannot read, or a write that fails, leaves the account as it was. A
harness whose login is not a file (omp keeps its credentials in SQLite) is
refused.
It only finds a login kept in that file. Claude Code on macOS keeps its login
in the Keychain, and a harness can be pointed at another home by its own
environment variable (CODEX_HOME); in either case --import says what it
looked at and finds nothing. That is also why a launch offers --import only
when there is a login there to copy.
It is a copy of one login, not a second one. An API key is a key, and two copies of it work side by side. An OAuth login is a refresh chain: when the provider replaces the refresh token on each use, whichever copy refreshes second is holding a spent one and is logged out. If the host and the sandbox both need to stay logged in, give the sandbox its own login with a device code. A later login on the host does not reach the account either way.
What stops a launch
Section titled “What stops a launch”-
Not being logged in is fine. The harness prompts, which is what you want before your first
omh auth. omh says so first, because the harness’s prompt names the harness’s own login, and that login may be one that cannot finish in a sandbox:omh: no codex account — starting logged out. `omh auth codex` to log in, or `omh auth codex --import` to copy this machine's login -
An account you named and do not have stops it. Running with no credentials produces a session that is logged out for reasons nothing explains.
-
Two identities and no stated preference stops it too. Guessing would send work traffic through a personal account and never mention it.
-
Account names are validated as single path components. Credentials mount writable, so
omh auth claude --name ../../..would otherwise resolve to~and hand the agent your real credential store. -
The runtime’s exit status is checked before the filesystem. A container that failed to start leaves the previous credentials in place, which would otherwise read as a successful re-authentication.
The invariant this bends
Section titled “The invariant this bends”The writable mounts are a short named set — the worktree, credentials, carried files, the sandbox’s own git repository, the harness’s transcript directory, and two caches. Nothing else is.
This is the weakest part of the current security model, and it is weaker than
what sbx offers: there, secrets are injected at the egress proxy and the agent
never holds its own token at all. omh mounts a credential volume, so a
compromised agent can read and exfiltrate its own token.
Several accounts widens that gap rather than narrowing it. See risks and runtime backends.
Verifying
Section titled “Verifying”omh doctor probes the token path by attempting the temp-file-plus-rename that
every token save performs:
✓ token /home/agent/.claude/.credentials.json (atomic write)It is non-destructive by construction. See Troubleshooting.