omh — the safe way to run an agent against your repo
A sandboxed session, your setup already in it, and a branch you can review.
$ omh init # detects your stack, decides, reports. no questions.
$ omh new claude # sandboxed, curated, your setup already inside
$ omh s01 attach # open that same session in your editor
$ omh graph # browse your codebase as a graph
Status: early, and one harness deep. 0.15.0. Claude Code is the only
harness anyone has done real work through. opencode, omp and codex pass
omh doctor, which proves their paths are right and nothing whatever about
their behaviour — so declare once, switch harness is the shape the
architecture is built for and not yet a thing anybody has done.
What is verified is the loop below: a sandboxed session with your config already inside it, and a branch you can read before it touches your checkout.
0.15.0 puts your catalogue on a screen. At a terminal, omh settings
is a screen over ~/.omh: your defaults, then a page per kind of entry —
rules, skills, servers, commands, subagents, hooks. Set and unset a default,
open or create an entry in $EDITOR, add or remove a server, import from a
harness, search across pages, undo. Every change is the command line you would
have typed, run as one and shown with what it said, so there is no second write
path. It also says what the next omh init hands a new repo, and
omh settings use / unuse change it. Piped, or with --json, it is the
report it always was.
The host stops running what the sandbox wrote. Three ways it still could:
git over the sandbox's repository ran a clean filter, a nested repository's
config or a post-checkout hook the agent had left; notes and transcripts were
written and read through links the agent had made, and its text reached your
terminal unsanitised; and a relative core.hooksPath — husky's .husky/_ —
resolved inside the agent's worktree, so omh sNN commit, push, sync and
the git status behind omh s ran hooks the agent wrote, as you. Your own
hooks still run; ones the agent could write are set aside, and commit and push
say so.
Things that did damage and said nothing. omh sNN attach could replace
~/.ssh/config with one line; two launches of one session could remove each
other's container; omh sNN rm could report success with the agent's commits
left behind, and deleted work the agent had told its index to ignore;
omh new could reopen a branch an earlier rm kept, old commits and all; one
launch made your own checkout ignore new AGENTS.md, CLAUDE.md and
.mcp.json files; a refused sync said nothing had changed after rewriting
the worktree; and a checkout whose directory has a space or an accent could not
launch at all.
Every pin moved — Claude Code 2.1.300, Codex 0.162.1, oh-my-pi 18.10.4,
opencode 1.19.0 — so each harness image rebuilds on its first launch.
omh upgrade now asks again which of a repo's provides apply before it builds.
The docs are a site, at docs.ohmyharness.sh.
And cargo install omh builds on Rust 1.85, the floor omh declares.
0.14.1 fixed three things nobody could see. No editor could attach to
any session — sshd dropped every connection before authentication, because
omh's container dropped the one capability its privilege separation needs, and
omh reported it as the editor failing to open. A fresh install's omh doctor
ended red on a memory server it had never built, and the row said the server
answered wrongly rather than that nothing was there. And a set of session
lifecycle paths could lose work: sync conflating a branch tip with its old
baseline, a removal that could not see uncommitted work when shadow state was
missing, a migration that left git worktrees unusable, a carry-in that
overwrote a symlinked destination.
0.14.0 made what a session learned reach the next one. The note store
had two layers — one committed, one yours — and a finding stayed invisible to
every other session until somebody committed, pushed and merged it. There is
one store now, at ~/.omh/memory/<repo>, shared by every session of the repo
and never committed: write a note and the next session can read it, the way its
code graph already worked. omh memory promote is gone along with the layer it
moved notes between, and a store recorded under the old path moves itself the
first time 0.14 runs.
The sandbox can merge again. omh's base image was Debian 12, whose git —
2.39.5 — has no merge-tree --merge-base. Any repo whose own checks use a
modern git failed inside the sandbox for reasons that named anything but git:
eleven of omh's own tests did, and none of them said the word. The base is
Debian 13 now, with git 2.47.3, and omh doctor asks the image the same
question so the next one of these is a single row rather than a test run.
Everyone rebuilds their image once. Codex and Claude also stop asking to
approve omh's own MCP tools: omh put them there, so the prompt asks you to
vouch for a decision omh already made.
0.13.0 moved every pin to its current release — Claude Code 2.1.274, Codex
0.154.0, oh-my-pi 18.2.3, opencode 1.18.31 — and each adapter was re-read
against it, which found two claims the new releases broke: Claude Code's native
builds search through Bash, so graph-first is named as dropped rather than
shipped to fire on nothing, and opencode logs in with /connect.
Codex you can log in to, and see into. The pinned Codex's only sign-in
waited for a browser redirect inside the sandbox, where no browser arrives.
omh auth codex
now takes a device code, or --import copies the login this machine already
has — read in full before anything is written, and offered only where there is
one to copy. Codex runs omh's hooks, rendered into the system layer it trusts
without a hash, and omh sNN reads its rollouts for what the agent did. An
account can name one
harness's login — omh set account codex:work — where it used to be one name
every harness had to share. And two more reads that could not look now say
so: an unreadable worktrees/ no longer makes omh s print no sessions, and
a graph entry omh sNN rm could not drop is no longer reported gone.
0.13.2 let Codex act. Codex runs every shell command and file edit inside its own bubblewrap sandbox, which cannot start inside omh's container, so under 0.13.1 an agent could read nothing and change nothing. omh now turns that sandbox off; the container is the sandbox, as it is for every harness.
0.13.1 fixed two launches 0.13.0 broke. opencode died on start —
EACCES creating ~/.local/state under a directory the image had left to root
— and Codex's interactive app could not save that it trusts /work, because
omh mounted a file where Codex rewrites its own config. Codex now finds /work
trusted and its config writable, and
omh doctor asks first whether the
harness starts at all, which every other row it has would have missed.
What isn't done is a real list, not a modesty ritual.
What you actually get
Running omh new claude instead of claude buys five things:
A sandbox that protects your repo, not just your host. The agent works in a
git worktree on its own branch. Your checkout is never mounted. Review with
omh s01 diff, ship with omh s01 commit and omh s01 push,
discard by deleting a branch. You never go near the worktree directory itself.
sessionsis the command;sis its alias, and a session id can lead the line. Soomh sessions --session s01 diff,omh s -s s01 diffandomh s01 diffare three spellings of one thing. The short ones are used from here on.
The agent gets git too — its own repository, holding one commit and none of your
history, so stash and reset --hard are its to use. omh s01 commit --keep
brings its commits onto your branch with the messages it wrote — all of them in
order, or the ones you name: omh s01 commit --keep 1,3-4.
Several sessions, from one place. omh s is every session with its state,
how far behind trunk it has fallen, and the files two of them are both about to
change — the collision you would otherwise meet at merge time. omh s01 is that
same row, scoped to one.
$ omh s
s01 omh/s01 stopped 2 uncommitted
s02 omh/s02 stopped 2 uncommitted
s01 and s02 both change shared.rs
omh s01 sync brings trunk in, merged on the host rather than inside the
sandbox. A conflict still lands in the worktree with its markers — labelled
main and s01, so which side is yours is obvious — and omh s01 commit refuses
to land a file that still holds them. An agent that commits nothing leaves a
timeline anyway: omh photographs the worktree at the end of every turn, and
omh s01 log --turns reads them back.
Your setup, declared once. Rules, skills, MCP servers, commands, subagents
and hooks live in one place and are rendered into whatever shape each harness
reads — a harness is a TOML file, not a code change. Whether that survives
switching harness is the claim this project has not yet earned: opencode and
omp render and pass omh doctor, and nobody has worked in them. Believe the
part you can check, which is that your setup is in one place and
omh eject hands it back.
A code graph that is current and actually used. Indexed per session, refreshed after every turn (0.14s), with hooks that point the agent at it when it is about to grep or read a whole file.
Your editor attached to the same place. omh s01 attach opens VS Code, Zed,
Cursor or Neovim over SSH into the sandbox — one dependency tree, shared with
the agent, instead of a second one on your host that silently diverges.
Install
Requires Docker and git.
$ brew install mindsers/tap/omh
macOS and Linux, arm64 and x86_64. brew upgrade keeps it current afterwards,
which is the part the script below cannot do.
Without Homebrew:
$ curl -fsSL https://ohmyharness.sh/install.sh | sh
Picks the build for your machine, checks it against the published
SHA256SUMS, runs it once to confirm it works here, and moves it into
~/.local/bin. A failed install never replaces a working omh. Read it first
if you would rather — it is one file, and that URL is a redirect
to exactly that file, so there is only ever one of it. Re-run it to update.
From source, which needs Rust 1.88+:
$ git clone https://github.com/mindsers/ohmyharness && cd ohmyharness
$ cargo build --release
$ cp target/release/omh ~/.local/bin/ # or add target/release to PATH
Quick start
$ cd ~/code/your-project
$ omh init
omh init — decided, asked nothing
harness claude (found on your host)
image omh/claude:b4edd8fd15d4d669 (already built)
image omh/claude:8eae0d5c1511fa89 (this repo's toolchain)
stack rust (from Cargo.toml)
hooks 2 selected (4 more in your catalogue)
provision rust/linker
provision rust/toolchain
memory 2 notes written, 0 already there
graph indexing in background → omh-cache-your-project-a8c4d1cd
catalogue /Users/you/.omh
this repo /Users/you/code/your-project/.omh (committed)
base set (2026.08)
omh-codegraph structural queries instead of re-grepping the repo every task
omh-memory what a session learned outlives it — a removed session leaves no transcript to grep
omh why <name> what it costs, what was considered instead, how to remove it
next
omh new claude start a session
omh s list sessions, each by its id
omh sNN resume rejoin one later
omh sNN attach open it in your editor
init decides and reports — it never asks. Every question is hassle the tool
promised to remove, and most answers are already lying around: manifests name the
stack, git log names what you work on, the README names the project.
Then log in once and go:
$ omh auth claude --name personal # runs the harness's own login, captures it
$ omh new claude # sandboxed, logged in, configured
omh new hands you the harness. Close the terminal whenever you like — it runs
under dtach, so the agent keeps going and omh s01 resume puts you back.
Getting the work back
When the agent has done something, you are on the host looking at a branch:
$ omh s
s01 omh/s01 up 2 uncommitted
$ omh s01 diff
README.md | 2 +-
src/main.rs | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
$ omh s01 commit -m "Add a greeting"
committed to omh/s01 (1 commit on the branch)
$ omh s
s01 omh/s01 up 1 to push
omh s01 diff -p is the patch, omh s01 log is what the agent committed inside
the sandbox, and omh s01 push puts the branch on origin under a name a
reviewer can read. Nothing here touches your checkout — the work is on
omh/s01 until you merge it, and omh s01 rm throws the whole session away.
The problem
A good agentic setup in 2026 is a pile of parts: a harness, rules, skills, MCP servers, a sandbox, a code index, hooks, credentials. Each is a rabbit hole, so most people stop at "installed Claude Code, wrote a CLAUDE.md" — not from inability, but because assembling the rest is a research project nobody has budget for.
The ecosystem's answer has been catalogues: 23,600+ skills and 12,700+ MCP servers. That is a problem statement, not an opinion. Nobody can evaluate 23,600 of anything.
Where this is going
omh is built to be a distribution. Debian didn't write the kernel; oh-my-zsh didn't write zsh — their genius was that installing them gave you a good system immediately. The value is curation, integration and defaults, and the metric is decisions removed, targeting zero.
That is the intent, and it is why the architecture looks the way it does:
adapters are data so a harness is a TOML file, the base set is a versioned
manifest so omh why and omh init cannot disagree, and every entry has to
say what it costs. It is not yet a description of what you get. The
curated set is eleven entries across three features, with five rejections
recorded beside them, and one person picks all of it —
risks names that, rather than any technical problem,
as the thing most likely to kill this project. The submission
standard is published so it does
not have to stay that way.
Commands
omh init set this repo up (once)
omh upgrade apply a newer omh: refresh and rebuild
omh new <harness> [-- args…] start a session, run an agent in it
omh auth <harness> [-n <name>] log in once; repeat for several accounts
omh set <key> <value> a setting, or a feature on|off — this repo
omh unset <key> drop it, or hand the feature back to omh
omh use|unuse <capability> <name>
omh use skills tdd · omh use --all
omh settings [set|unset|edit|rm|mcp] …
you: your defaults, and your catalogue
omh info [--repo] this machine · --repo, this checkout
omh why <thing> who put this here, and on what grounds
omh doctor [--harness <name>] prove a harness really sees your profile
omh graph [--stop] browse the code graph in a browser
omh memory [remember|stale|lint|rm] …
notes that outlive a session
omh import <capability> <harness>
bring a setup you already have into omh
omh eject <harness> --to <dir> write out the raw config and step aside
Sessions
The loop the rest of it exists for. sessions is the noun, and everything below
takes the session id in front of it:
omh s every session: state, drift, collisions
omh s01 that one, and what to run next
omh s01 log [--turns] what the agent committed in the sandbox — or,
if it committed nothing, what omh photographed
omh s01 diff [n] [-p] what changed: the whole session, or checkpoint n
omh s01 commit [-m …] land its work as one commit on the branch
omh s01 commit --keep [n,m-o] or replant the agent's own commits, with the
messages it wrote — all, or the ones you name
omh s01 push [name] that branch to origin, named for a reviewer
omh s01 sync bring trunk in, merged on the host
omh s01 resume [harness] rejoin it, running the harness it ran before
omh s01 attach [editor] open it in your editor, over SSH
omh s01 down stop the sandbox; the worktree and branch stay
omh s01 rm remove it — container, worktree and staging
Three shorthands, and they compose: sessions is s, a leading session id
replaces -s, and every session verb takes one. So omh sessions -s s01 diff,
omh s -s s01 diff and omh s01 diff are the same line — use the last.
Everything after -- belongs to the harness: omh new claude -- --verbose
passes --verbose to claude, not to omh.
--dry-run runs everything and writes nothing; a command that cannot yet
answer it refuses the flag rather than running.
Commands is the full reference — every flag, and what each command prints.
How it works
Sessions
A session is a running container, a git worktree, and a branch — which many harnesses take turns inhabiting.
omh new claude ───────┐
omh new opencode ─────┼── exec ──┐
omh s01 attach ───────┘ (ssh) │
▼
┌───────────────────────────────────────────────────────┐
│ SESSION omh-<repo>-s01 detached, long-lived │
│ sshd 127.0.0.1 ──── your editor attaches here │
│ /work ← worktree, the code you get back │
│ staged profile, read-only │
│ graph cache ← volume keyed by REPO, not harness │
└───────────────────────────────────────────────────────┘
Harnesses run under dtach, so closing your terminal doesn't kill the agent —
omh s01 resume puts you back in the one you left running.
One catalogue, and it is personal
~/.omh/
rules/ skills/ commands/ subagents/ hooks/ the only place these live
mcp.json
default.toml what a new repo starts from
A repo holds configuration, and one kind of content:
<repo>/.omh/settings.toml committed: settings, and which of omh's features are on
<repo>/.omh/settings.local.toml gitignored: your overrides, and the secrets the other must not hold
<repo>/.omh/memory.toml committed: how the note store keys and expires
<repo>/.omh/hooks/ committed: hooks that only make sense in this repo
<repo>/AGENTS.md the project's own rules — tracked, and actually read
A project cannot declare a skill, an MCP server, a command or a subagent; it
names ones from your catalogue. Hooks are the exception, being the one
capability whose scope is genuinely the repo — cargo test here, pnpm test
next door, one name and two bodies.
Naming them is one table, and one mechanism — an allowlist, so removing something is deleting its name:
# <repo>/.omh/settings.toml
[use]
rules = ["tdd", "commit-style"] # for rules, the list is the order
skills = ["review-diff"]
mcp = ["*"] # keep following the catalogue as it grows
Absent means everything, so upgrading changes nothing and a new checkout is
useful before it's configured. Settings are omh set <key> <value>, and the
key decides which file it lands in: one that can name a credential is kept out
of git, everything else is committed so a teammate cloning gets it —
Configuration has the rest.
A hook can also refuse a call instead of advising around it — tdd-guard
and config-guard ship this way, opt-in (omh use hooks tdd-guard), never
turned on by omh init itself. One name covers every stack it knows (go,
python, node), so a monorepo names it once. Discipline, not containment: the
worktree is what actually contains a session; a guard is a reminder at the
moment its rule is broken —
Guards, and their bypass.
The base set is data too, and it has to justify itself
omh's opinion lives in a versioned file, not in the binary — every launch
generates from it and omh why explains from it, so the two can't disagree
about what is installed or why:
$ omh why codegraph
codegraph — an omh feature, in the base set since 2026.06
gathers omh-codegraph structural queries instead of re-grepping the repo every task
graph-orient the agent is given the module map instead of discovering it by reading files
graph-first structural questions answer in one call instead of an iterative grep
graph-read one symbol instead of a whole file, when the file is big enough for that to matter
graph-refresh the graph describes the code as it is now, not as the session found it
graph-rules a graph the agent has not been told about is a graph it never queries — the four hooks fire while it is already doing something, and this is what is true before it starts
costs omh-codegraph 0.46s to index this repo, cold measured 2026-08-06
index_repository --mode fast, 821 nodes / 3813 edges, in the sandbox
3.4 MB on disk measured 2026-08-06
the graph volume after a cold index of this repo
graph-orient 2,300 B per context rebuild measured 2026-08-06
get_architecture with four targeted aspects (2,138 B of payload) plus the hook's own framing prose
graph-first 243 B per grep measured 2026-08-07
the nudge string for a `repo-sNN` project name, computed from the literals the hook ships — see the_grep_nudges_declared_cost_matches_the_string_it_ships
graph-read 1,511 B when it fires measured 2026-08-06
get_code_snippet for one symbol, against reading the enclosing source file whole — this repo's largest was over 20x that. The file size is deliberately not recorded: it moves on every edit, and an earlier version of this line was already stale on the commit that wrote it.
0 B when it does not measured 2026-08-06
silent unless the target is source over 8,000 bytes
graph-refresh 0.14s per turn measured 2026-08-06
incremental re-index of a session worktree at Stop
graph-rules 673 B in the agent's context, every turn measured 2026-08-12
the section as shipped, byte for byte. Computed by `every_rules_section_costs_what_it_says` against the string omh sends rather than typed in — a hand-written cost and the prose it describes have no relationship a test can check
instead of omh-codegraph gitnexus PolyForm-Noncommercial licence
codegraphcontext needs a Neo4j service running
@sdsrs/code-graph close second; needs a node runtime rather than a static binary
graph-orient the `overview` aspect 6,173 B for the same orientation the four targeted aspects give in 2,138
graph-first blocking grep outright grep is right for a literal string, and a hook that blocks correct work gets disabled
graph-read nudging on every Read Read is the most frequent tool there is; a nudge on every call becomes noise the model tunes out
graph-refresh indexing once at init a session's worktree is not the checkout it started from; the graph would describe code the agent has since changed
graph-rules the graph-orient hook alone orientation is re-paid at every context rebuild and describes the modules; this names the tools and the project variable once. Different claims, and the hook's 2,300 B is the one that repeats
remove omh set codegraph off — the feature, its server and its hooks together. Nothing is uninstalled and the next repo gets it back; `omh settings set codegraph off` does the same for every repo `omh init` sets up after
A feature is all or nothing: every part above is generated at
launch and switched together, and none of them is a file of yours.
answered from ~/.omh/base/2026.08.toml · 2026.08
Cost is measured; benefit is argued. Those are different kinds of claim and
the output never blurs them — every number carries the date it was taken and the
method, while because is a judgement you're free to reject.
The four fields aren't a convention, they're a test: an entry that can't say
what it costs, what it buys, what was considered instead and how to remove it
fails the build. omh why answers for things omh rejected too, so a candidate
turned down over its licence isn't re-litigated every time someone rediscovers
it — and it offers no rationale at all for something you added, because it has
none. Telling those apart is the entire point.
Adapters are data
Adding a harness is a TOML file, not a code change — where each capability lands, how it is rendered, and the command that proves it loaded:
[capabilities.mcp]
path = "/work/.mcp.json"
render = "mcp-json"
verify = "claude mcp list" # and how omh knows it worked
ready = "Connected"
An absent key means the harness cannot do that thing. Degradation is a missing map entry rather than special-case logic, and it is announced once:
$ omh new opencode
omh: opencode on omh/s01 — dropped hooks: graph-first (no `search` tool),
graph-orient (no `session-start` moment),
graph-read (no way to inject text before a tool runs)
Editors work the same way — ~/.omh/editors/zed.toml is four lines.
Adapters is the whole schema.
The code graph
Every session is indexed into a graph (codebase-memory-mcp, MIT, a static binary with no runtime or database). Four hooks make it something the agent uses rather than something merely installed:
They are nudges, never walls: grep is right for a literal string, and a hook that
blocks correct work gets disabled. graph-read stays silent unless a symbol
lookup would genuinely be cheaper.
² No shipped harness names a search tool today — Claude Code searches through
Bash since its native builds — so every launch reports graph-first as dropped.
$ omh graph
graph at http://127.0.0.1:50257
omh graph --stop
every session's graph for this repo, in one place
Credentials
$ omh auth claude --name personal
$ omh auth claude --name work
$ omh set account work # which one this project uses
$ omh new claude
Accounts are per harness, and which one a session uses is a project-level setting — because that is how it actually varies: this repo is work, that one is personal. Ambiguity is an error, never a guess: two identities and no stated preference stops the launch rather than sending work traffic through a personal account.
Files your worktree needs
A worktree holds only tracked files — no .env, no certs, so the agent lands
somewhere that cannot run your app.
carry_in = [".env.local", "certs/"]
An explicit allowlist, because this is the only path by which a secret reaches the agent. A listed path that doesn't exist is reported, not skipped.
It is for files git does not track. A tracked path is already in the worktree, so listing one would replace the branch's copy with whatever your checkout holds right now — usually an uncommitted edit, on the one path a secret travels. omh says so at launch and does not copy it.
Verify it yourself
omh doctor is the only thing that can prove an adapter is right. It launches
the real image with the real mounts and inspects the paths the harness actually
reads — including asking claude mcp list whether the servers loaded, rather
than trusting that a file landed:
$ omh doctor
checking claude in omh/claude:8eae0d5c1511fa89 — no account, so credentials go unchecked…
✓ starts claude --version
✓ rules /work/CLAUDE.md
✓ skills /home/agent/.claude/skills
✓ mcp /work/.mcp.json
✓ mcp-loaded /work (claude mcp list: Connected)
✓ commands /home/agent/.claude/commands
✓ subagents /home/agent/.claude/agents
✓ hooks /home/agent/.claude/settings.json
…
Above those, in the same table, are the host's rows: whether the container runtime is answering, the stacks detected here, settings omh does not read, what omh has left behind, which omh set this checkout up, disk, and the host's git. They are gathered before any container work, so a machine that cannot build a sandbox still gets them instead of a single error.
A clean run ends all N checks passed — claude's adapter paths are verified;
anything else fails the command and the tally goes to stderr. With an account
chosen there is a credential check too, and the header names which account it
used.
A green unit suite proves omh mounts a path faithfully; it proves nothing about
whether anything reads it. That gap is what doctor closes.
What isn't done
Known rough edges: the graph store is shared across sessions of one repo, so an
agent can query another session's graph (mitigated, not prevented);
.claude.json is a file mount that cannot be atomically replaced; omh sNN rm
keeps a session branch whenever omh cannot prove trunk already holds its work,
so a squash it could not match leaves a branch behind and says so; upgrading to 0.8.0 leaves
the old omh-cache-<name> volume and omh-<name> network behind and nothing
reports them (docker volume ls | grep omh-cache- finds them); and omh s
lists any directory under a session's worktree root as a session, so stray
clutter there shows up as one.
Contributing
$ ./scripts/check.sh --all # format, lints, suite — `--all` needs a container runtime
$ omh doctor # the only thing that verifies an adapter
Two rules do most of the work, and CONTRIBUTING.md
has the rest with the invariant list. To propose something for the base set,
what an entry has to say is the
bar — four fields, enforced by the build rather than by review. TDD,
always — a green suite is not evidence on
its own, so a bug fix's test goes red before the fix lands. And adapters
assert facts about external software: almost every bug this project has
shipped lived at that boundary and none was catchable in-process, so if you
change an adapter, run omh doctor.
Documentation
Full docs are at docs.ohmyharness.sh.
Start with Getting started — install to first session. Commands is the whole surface and what each one prints; Configuration, Sessions, Accounts and Editors cover the parts in depth.
Read the pages under Understanding omh before changing architecture — most record something that was tried and cost something, and Risks states plainly what is still weak.
Licence
MIT — see LICENSE.
One dependency, option-ext (transitive via dirs), is MPL-2.0; everything else
in the tree is Apache-2.0, MIT, BSD, ISC or Unicode-3.0.