Start a new project with Morph
A 20-minute walkthrough that takes you from an empty directory to a Morph repo with metric-bearing commits, a recorded agent session, and a clean revert. Along the way you'll meet the parts of Morph that don't exist in Git: runs, traces, and behavioral commits.
01Before you start
Morph is a behavioral version control system that rides on top of
Git. Every git commit on this project will be
mirrored into a Morph commit automatically; on top of that Git
backbone, Morph layers the things Git can't represent —
runs, traces, and
behavioral commits. So the tutorial starts with a
one-line git init and never asks you to think about Git
again. The interesting work all happens through morph.
If you already have a Git project with history and teammates and want to layer Morph on top of that, the right path is Tutorial 2.
You'll need four things on your machine before you start:
morphandmorph-mcpon yourPATH— see installation.- Git 2.28 or later (for
git init -b mainsupport) and a configured identity.git config --global user.name && git config --global user.emailshould each print something. If they don't, set them withgit config --global user.name "Your Name"andgit config --global user.email "you@example.com"— Morph mirrors every commit throughgit commit, which refuses without an identity. - Python 3 with
pytestavailable (python3 -m pip install --user pytest). - Cursor, Claude Code, or OpenCode — the agent step is the whole point of recording, so we treat the IDE as a real prerequisite. The screenshots and commands here use Cursor; the others differ only in
morph setup <ide>. - A free directory to scribble in. We'll use
~/morph-tutorial.
morph --version should print something like morph 0.48.0 (built …).
If it doesn't, fix that before going further — the rest of the tutorial assumes the binary is on your PATH.
02Build a tiny project
We need something small enough to fit in a tutorial and interesting enough to write tests against. A currency converter works: a function and three tests, one Python file each.
That's the only Git command in the tutorial. -b main
pins the initial branch to main so it matches Morph's
contract regardless of your host's init.defaultBranch
setting. From here on, every Git operation will happen for you:
morph commit wraps git commit,
morph add wraps git add, and the four
hooks Morph installs in the next step keep the two histories in
lockstep.
git init:
morph init will detect there's no .git/
and prompt "morph requires a git repository here. Run
`git init` for you? [y/N]". Answering yes is equivalent to
running git init -b main yourself, or you can pass
morph init --git-init for the same shortcut without
the prompt.
Create converter.py:
And test_converter.py:
Sanity-check that the tests pass before we bring Morph into the picture:
03morph init
Now turn this directory into a Morph repo:
Four lines worth reading carefully:
- "bound to empty git repository" — Morph noticed the
.git/you just created and snapshotted it as the starting point. With a non-empty repo it would say "bound to git HEAD <sha>". - "installed 4 git hook(s)" —
post-commit,post-checkout,post-merge,post-rewrite. Each is a tiny shell stub that callsmorph hook <event>and exits zero whatever happens, so a broken Morph never blocks a Git commit. - ".morph/ added to .git/info/exclude" — local-only.
git statuswon't see.morph/, even with no.gitignorechange. Morph state never travels through Git. - "teammates not using morph are unaffected" — you'll only feel this in Tutorial 2 (where there are teammates), but it's the same setup for both.
The .morph/ directory is the moral equivalent of .git/ — a
content-addressed Merkle DAG of objects, plus refs, config, and (this is
where it diverges from Git) folders for runs/, traces/, prompts/,
and evals/. Those are the things Git can't represent.
Now look at the default policy. This is the most opinionated thing Morph does, and it's better to meet it head-on than to be surprised by it later:
The default required_metrics is empty — Morph rides on
top of Git, and every git commit automatically
mirrors into a Morph commit through the post-commit hook. If the
policy required metrics on every mirror, every plain
git commit would fail. The exempt_origins:
["git-hook"] carve-out is the explicit way to say "passive
mirrors don't have to carry evidence; only commits I make through
morph commit do."
For the rest of this tutorial we want the gate. Tell Morph to require
test evidence on every morph commit:
That's the project's promise to itself: every commit must arrive
carrying tests_total and tests_passed as
evidence, and merges will be gated on dominance — the
merged code has to be at least as good as both parents on every
declared metric. You can relax it later with
morph policy require-metrics (pass no names to disable
the gate entirely), but the default once you've turned it on is
"you don't ship code without proof it works." We're going to honor it.
04Your first commit (the honest version)
Let's stage everything and try the most obvious commit:
That's the policy you just turned on biting us. The error tells you exactly what to do — there are three real ways out:
--metrics '{"tests_passed": 3, "tests_total": 3}'— supply them by hand.- Run the test suite under Morph (we'll do this in step 8) so the metrics flow in automatically.
--allow-empty-metrics— the audited escape hatch. The commit still gets recorded, just without behavioral evidence.
For a scaffold commit, option 3 is the right call: there's nothing interesting to evaluate yet, and the commit message says so. From now on, though, we'll always have evidence.
Two things just happened in one step: morph commit shelled
out to git commit (that's the git: a92430d4f0c9
line — Git's hash for the same change), then mirrored the new Git
commit into a Morph commit (f7077862) tagged
(cli) to distinguish it from the passive
(git-hook) mirrors that ride along plain
git commit. git log and morph log
stay in lockstep from here.
morph status:
if you run morph status after a morph add, you'll still see your
files listed under "Changes not staged for commit." That's a label
quirk — Morph's status compares the working tree to HEAD, not to the
staging area, so the section header is misleading. Your files are
staged. The next morph commit will pick them up.
05Wire up your IDE
One command installs the MCP server config, the recording hooks, and the behavioral-commit rules for your IDE. Pick the one you use:
What just happened: the hooks parse the agent's full transcript — every prompt,
response, tool call, file read, file edit, shell command — into structured
Trace events stored under .morph/traces/. Recording is
always-on. You don't need the agent to remember to record; the hooks see
the transcript and Morph picks up the rest.
From here on, when an agent works on this project in your IDE, every session becomes an immutable Run with a full Trace. Restart your IDE so it picks up the new MCP config, then come back to the terminal.
06Edit by hand
Make a small change so we have something to commit. Add a docstring
to convert, or anything trivial — the goal is just a diff:
We could stage and commit this right now with another
--allow-empty-metrics, the way we did the scaffold — and the human
loop on its own really is identical to Git's. But that's not why you came.
The next two steps wire up the part Git can't do: capture evidence
alongside the diff, and let it flow into the commit automatically.
07Capture metrics with morph eval run
Your test runner already produces the numbers Morph's policy wants —
pass / fail counts, totals, pass rate. morph eval run is just a
wrapper around any test command: it shells out, captures stdout, parses
it (cargo, pytest, vitest, jest, and go are auto-detected), and writes a
Run object into the store linked to HEAD. The Run carries
the parsed metrics and a Trace of the invocation. It also drops a
.morph/LAST_RUN.json breadcrumb that the next morph commit
knows to pick up.
One ordering detail matters: stage first, then run the
eval. The breadcrumb captures the staging-index fingerprint at
the moment morph eval run finishes, and the next
morph commit only auto-attaches if that fingerprint is
still fresh — so any edit between the run and the commit
invalidates the breadcrumb.
That hash is the new run. You can inspect it directly:
The metrics from your test run are now sitting in a Run object,
waiting to be attached to a commit. morph eval gaps tells you
what evidence is still missing relative to HEAD:
Two gaps. The first (empty_head_metrics) is the one we'll close
in the next step — it just means HEAD hasn't yet been
stamped with metrics. The run carries them; the commit doesn't yet.
empty_default_suite:
this gap will stick around for the rest of the tutorial — that's expected.
It means we haven't registered a YAML acceptance suite, a separate
Morph concept where you write down what the project promises in plain
English. It's optional for a single-branch project (your test runner
already produces the numeric metrics the policy needs), but it earns its
keep when you start merging branches and want the merge gate to compare
richer claims than raw numbers — see
MERGE.md
and
EVAL-DRIVEN.md
for the merge-gate and case-provenance flow. Ignore the gap for now.
-q.
The pytest parser keys off the === N passed in Xs === summary
line, which pytest -q in recent versions doesn't emit. If
morph eval run warns "no metrics extracted," drop the -q.
morph config commit.test_command "python3 -m pytest"
once per repo, and from then on plain
morph commit -m "…" runs your suite, parses
the metrics, and attaches them — one command, same
evidence. --no-test opts out for a single commit;
--rerun forces a fresh run when the breadcrumb is
stale.
08Commit with evidence
The change is staged, the run is recorded, the breadcrumb is fresh.
Now commit. You don't need --from-run or
--metrics — Morph notices the
LAST_RUN breadcrumb left by morph eval run and
attaches its metrics to the commit automatically:
That "attaching evidence from run …" line is the moment of payoff.
The commit you just made is no longer a snapshot — it's a snapshot
plus a behavioral claim, with the run that produced it linked as
evidence.
See for yourself. morph log defaults to a compact one-liner; the
metrics live one level deeper, on the commit object itself or in the
--json view of the log:
Three new fields, all things Git can't represent.
eval_contract carries the behavioral claim and the suite
it was evaluated against. contributors distinguishes the
human (role: "human-author") from the
morph-eval-run agent that produced the metrics
(role: "primary") — if you'd used --from-run
with a recorded agent session instead, the agent would land there
alongside you. evidence_refs point back to the runs that
justify the metrics.
The git_origin_sha matches the git: e44e54801333
line from the commit output — that's the bridge between the two
histories. morph_origin: "cli" distinguishes this from
passive git-hook mirrors that ride along plain
git commit. The suite hash
5abb5131… is Morph's canonical empty-suite hash — it's
the stand-in until you register a real one in Tutorial 3.
09Let an agent take a turn
Open this project in Cursor (or your IDE of choice) and ask the agent for something concrete:
While the agent works, the hooks you installed in step 5 are quietly
recording its transcript into .morph/traces/ — every prompt and
response, every tool call, every file read, every file edit, every
shell invocation. You don't have to do anything; recording is
background-noise.
When the agent finishes, switch back to the terminal. Your working tree should look something like this:
That "Morph activity" line is new. Two of those runs are from your
earlier morph eval run calls; the third is the agent's session
— a Run object with a Trace containing every event from the IDE turn.
(The "0 cases registered" is the same empty_default_suite
gap from step 7 — still expected, still fine.)
10Inspect what the agent did
This is the part Git literally cannot do. morph inspect
(v0.45+) is the read surface for recorded agent work — runs, prompts,
tool calls, file edits, aggregated by trace.
Drill into a specific run to see its grouped steps:
Or look at it through the trace lens, which understands phases and target-context:
Every one of those events is content-addressed and immutable. Six months from now, when someone asks "why does the converter handle GBP this way?", the answer is a single hash away.
Now finish the loop. The agent already ran pytest via shell:
events during its turn, but we want a fresh LAST_RUN breadcrumb
tied to the staged tree, so re-run it explicitly:
Three commits, two with metrics, one (the latest) with the agent's full
Trace attached as evidence. morph log --json will show you the
metrics inline if you want to verify.
--from-run here? The auto-attach
breadcrumb from §08 only fires when the staging-index fingerprint
hasn't moved since the run was recorded. The agent's tool calls
mutated files outside the standard "stage, then run" rhythm
(it edited and ran pytest from inside its turn), so
we record a fresh run and pass it explicitly. --from-run
<hash> is the always-works form — the run gets
attached to the commit and lands in commit.contributors
alongside you (with role: "primary" for the agent and
role: "human-author" for you). The auto-attach in §08
is just a convenience for the simple case.
11Make a regression, then revert
Morph is a full VCS, not just an additive recorder. Reverts work, and the interesting twist is that you can see the regression in the metrics before you decide to undo it.
Break something on purpose. Edit converter.py and change the EUR
rate to 0.95 (sloppy "fix" — it'll fail two tests). Then run the
eval and commit:
The commit succeeded — Morph's policy requires evidence, not good
evidence. The bad commit is recorded with its real metrics, exactly as it
should be (suppressing it would defeat the point of behavioral version control).
You'd see this regression in morph log --json:
Now revert it. morph revert creates a new commit that undoes the
target. One quirk to know: revert doesn't update your
working tree on its own — you'll need morph checkout <branch> to
bring the files back into line:
Both the bad commit and the revert are preserved in history — the bad commit is still inspectable, its run still shows the regression, and the new HEAD is back to a green metric. The Trace of why you made the regression and how you backed out is part of the repo forever. That's the point.
12Browse it visually
The CLI gets you everywhere, but for showing the result to a teammate (or to yourself, after a long session), the hosted browser UI is a much nicer way to see the graph. Run it from the repo:
Open that URL. You'll see the commit graph laid out with metrics on every
node, click into a commit to see its eval contract and evidence refs,
click an evidence ref to see the run, click into the run to see the trace,
click a trace event to see the prompt or tool call. Same data morph
tap showed in the terminal — just easier to share.
Stop it with Ctrl-C. morph serve is read-only over the
underlying repo; nothing it does mutates state.
13Where to next
You now have a project that's tracked end-to-end by Morph: every commit carries metrics, every agent session is a Run with a Trace, the working tree can be reverted cleanly, and the whole graph is browsable. From here:
- Tutorial 2 — Adding Morph to an existing Git project: run Morph alongside Git as a behavioral safety net.
-
MERGE.md
and EVAL-DRIVEN.md:
behavioral merge gating, dominance, case provenance, and what happens
when two branches genuinely conflict. This is where YAML
acceptance suites (the
empty_default_suitegap you saw throughout this tutorial) start earning their keep — they give the merge gate something more than raw numbers to compare against. (A standalone "Tutorial 3" page is on the roadmap; for now the authoritative writeup lives in those two docs.) - Eval-driven development: the full guide to spec-first workflows, suite evolution, and CI integration.
- The theory: pipelines, certificate vectors, partial orders — the algebra under the CLI.