Tutorial · Add Morph to an existing Git project

Morph alongside the Git repo you already use

You have a Python project on GitHub, the team uses Git, and you want the things Morph adds — runs, traces, metric-bearing commits — without changing anyone else's workflow. This walkthrough sets up reference mode: Git stays the source of truth, Morph rides on top, and every git commit is automatically tied to a Morph commit by hash.

Time: ~20 min Mode: Reference (Stowaway) Stack: Python 3 · pytest · Git · GitHub Difficulty: Intermediate

Who is this for?

If any of these sounds like you, the rest of this tutorial is worth your 20 minutes:

Curious about Morph

You want to try Morph on a real project — not a toy sandbox — without making a commitment for the whole team. Reference mode is local to your clone; you can rip it out at any time with rm -rf .morph && rm .git/hooks/post-* and Git keeps working as if Morph never existed.

Starting agentic coding

You're about to let an agent loose on an existing repo and you want runs, traces, and metric-bearing commits captured locally before any of that work goes to your teammates. Morph records every agent session as an immutable trace tied to the Git commits it produced.

Don't want to bother teammates

The rest of the team is on plain Git and you don't want to change that yet. Stowaway submode is invisible to anyone else — .morph/ is auto-excluded from Git, the four installed hooks live in untracked .git/hooks/, and git push/git pull ship only the Git tree. Teammates see exactly what they always have.

When you eventually do want to bring teammates onto Morph — sharing runs, traces, and certifications across machines — there's a clean path for that too. We cover it in §11, after you've gotten comfortable with the solo flow.

01Before you start

This tutorial assumes you've at least skimmed Tutorial 1 — we won't re-teach the eval-run-then-commit flow, just show how it interacts with a real Git repo, a remote, and (eventually) teammates.

You'll need:

  • morph and morph-mcp on your PATH (install guide). morph --version should print 0.48.0 or later.
  • An existing Git project with at least one commit. If you don't have one handy, follow Tutorial 1 first — it walks through scaffolding a tiny project with git init -b main and gives you a couple of commits to backfill against here.
  • Configured Git identity: git config --global user.name and git config --global user.email should each return something. Morph mirrors every commit through git commit, which refuses without an identity.
  • Python 3 with pytest — the example commands assume the same currency-converter shape as Tutorial 1, but any test runner Morph can parse (cargo, pytest, vitest, jest, go) works.
  • A GitHub remote (or any Git remote). We'll demonstrate that git push works unchanged.

02How it works, in plain English

Reference mode is a single sentence: Git owns your code; Morph watches Git through hooks and writes a parallel history of behavioral metadata next to it. Concretely:

  • Git is the only thing your team sees. Teammates clone, pull, push exactly like before. The hooks Morph installs are local to your clone (Git never tracks .git/hooks/), so other developers don't get them and don't notice anything.
  • Morph is your local sidecar. All Morph state lives in .morph/, which the installer adds to .git/info/exclude so a stray git add . can't accidentally commit it. .morph/ never travels.
  • Hashes are tied. Every Morph commit object carries a git_origin_sha field equal to the Git SHA it mirrors. Git's hash is the truth; Morph's hash is a content-addressed pointer to "the behavioral record for that Git commit." No special database, no separate ID space.
  • Recording is automatic. A post-commit hook fires after every git commit and mirrors the new Git commit into Morph. post-checkout, post-merge, and post-rewrite handle the other Git events. All four are passive — if Morph is broken or uninstalled, your Git keeps working.
Two flavors of reference mode exist: Stowaway (default, only you have Morph; passive observers) and Solo (the whole team adopts Morph; an extra pre-merge-commit hook gates plain git merge on behavioral dominance). This tutorial covers Stowaway end-to-end. We touch Solo briefly at the end — the full story is in docs/reference-mode.md.

03morph init

From the root of your Git working tree, run:

$ morph init . Initialized empty Morph repository in /…/converter/.morph/ bound to git HEAD d12b9aaa7fda installed 4 git hook(s) at .git/hooks/ (4 present, 0 already up to date) morph state is local to this clone (.morph/ added to .git/info/exclude) teammates not using morph are unaffected — your git workflow is unchanged

Four lines worth reading carefully. Each is a real promise:

  • "bound to git HEAD <sha>" — Morph snapshots which Git commit it started watching from. Pre-existing history before this point isn't mirrored automatically; we'll handle that in the next step.
  • "installed 4 git hook(s)"post-commit, post-checkout, post-merge, post-rewrite. Each one is a tiny shell stub that calls morph hook <event> and exits zero whatever happens.
  • ".morph/ added to .git/info/exclude" — local-only. git status won't see .morph/, even with no .gitignore change.
  • "teammates not using morph are unaffected" — the whole design goal in one sentence.

Peek at one of the hooks if you're curious:

$ cat .git/hooks/post-commit #!/bin/sh # Installed by morph (`morph init` / `morph install-hooks`). # Mirrors every git commit into a Morph commit with morph_origin=git-hook. [ "$MORPH_INTERNAL" = "1" ] && exit 0 exec morph hook post-commit >/dev/null 2>&1 || true

That's the whole machinery. The MORPH_INTERNAL=1 escape is how morph commit and morph merge shell out to Git without the hook recursing into itself, and the trailing || true is the guarantee that a broken Morph never blocks a Git commit.

If you're starting from an empty directory (no .git/ yet), morph init will detect that and interactively prompt: "morph requires a git repository here. Run `git init` for you? [y/N]". Pass morph init --git-init for the same shortcut without the prompt — convenient in CI or shell scripts. Either way Morph runs git init -b main (anchored to main so the rest of Morph's contract holds regardless of your host's init.defaultBranch setting).

04Mirror your existing history

Right after init, morph status tells you about the elephant in the room: any Git commits that existed before you ran morph init aren't mirrored yet.

$ morph status Reference mode (git ↔ morph) git HEAD: d12b9aaa7fda drift: 2 unmirrored git commits — run `morph reference-sync` last mirrored: (none — run `morph reference-sync --backfill`) Eval suite: 0 cases registered

This is drift, the central concept of reference mode. Whenever Git moves and Morph hasn't seen it, you're in drift. It's not an error; it's a state. Morph names it explicitly so you can decide what to do.

For pre-existing history, the right answer is --backfill:

$ morph reference-sync --backfill Synced 1 git commit. $ morph status Reference mode (git ↔ morph) git HEAD: d12b9aaa7fda drift: up to date 1 uncertified git-hook commit on this branch — run `morph certify` to attach evidence.

Two notes on what just happened:

  • Backfill mirrored from the init point forward, not all of history. That older "scaffold" commit before morph init stayed in Git only. This is intentional — behavioral evidence for code that existed before you adopted Morph would be made up.
  • The mirror is "uncertified." It carries the Git tree's shape but no metrics. morph certify attaches metrics retroactively if you want them; we'll see the more common pattern (commit with metrics in a single step) shortly.
About morph status in reference mode: you may see a "Changes not staged for commit" block listing files that Git already has committed. That's a known UX wart — Morph commits in reference mode have tree: null (the Git tree is the truth), so the working-tree diff has nothing to compare against. Trust git status for whether your tree is clean. The "Reference mode (git ↔ morph)" block at the bottom is the one that matters here.

05Plain git commit — automatic mirror

From here on, just commit through Git the way you always have. Edit a file, stage, commit:

$ git add converter.py $ git commit -m "add docstrings to converter" [main df21819] add docstrings to converter 1 file changed, 4 insertions(+)

That looks like an ordinary Git commit because it is an ordinary Git commit. The post-commit hook fired silently, and Morph now has a mirror:

$ git log --oneline | head -3 df21819 add docstrings to converter d12b9aa add basic conversion tests a92430d scaffold currency converter $ morph log --oneline 258a7e26 add docstrings to converter 9b3b2490 add basic conversion tests

Morph's hash 258a7e26 is different from Git's df21819 — they're hashing different things (Git: tree + parents + message; Morph: all of that plus the behavioral metadata Morph adds). The tie between them is git_origin_sha on the Morph commit:

$ morph show 258a7e26 { "type": "commit", "tree": null, "parents": ["9b3b2490…"], "message": "add docstrings to converter", "eval_contract": { "suite": "5abb5131…", "observed_metrics": {} }, "morph_origin": "git-hook", "git_origin_sha": "df21819ff45149f1062621c9afb0a8557ab0df86" }

That's the tying-of-hashes. Two important details:

  • tree: null. Morph doesn't duplicate Git's file tree in reference mode — the Git tree is canonical. Saves space, avoids two-systems-of-truth bugs.
  • morph_origin: "git-hook". This commit was created passively by the post-commit hook, not by anyone running morph commit. Morph keeps these labelled separately so the merge gate can tell active claims from passive ones.

06Pulling from teammates who don't use Morph

The whole point of Stowaway is that only you have Morph. Your teammates clone, commit, push exactly like before. So what happens when one of them pushes a slew of commits and you git pull them down?

Short answer: Morph mirrors every new commit automatically, with the right parent edges, and morph status stays accurate. The two pull flavors — fast-forward and rebase — route through different hooks but both Just Work.

Fast-forward pull (git pull --ff-only)

Suppose I'm on v1, the only commit. A teammate pushes five commits (v2..v6). I pull:

$ git pull --ff-only From github.com:you/converter f0f8a30..16058d3 main -> origin/main Updating f0f8a30..16058d3 Fast-forward a.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)

Git fast-forwards the ref and fires the post-merge hook. That hook calls morph hook post-mergesync_to_head, which walks first-parent ancestry from the new HEAD back to the last mirrored commit and mirrors every commit in the unmirrored span in topo order. The result:

$ git log --oneline 16058d3 teammate: a.txt v6 b545510 teammate: a.txt v5 a1a2fa1 teammate: a.txt v4 b06e8a5 teammate: a.txt v3 265dac0 teammate: a.txt v2 f0f8a30 initial: a.txt v1 $ morph log --oneline 19a6d04c teammate: a.txt v6 ba3647cc teammate: a.txt v5 61a27e0e teammate: a.txt v4 b6252e44 teammate: a.txt v3 76d86fc9 teammate: a.txt v2 7ef4fc34 initial: a.txt v1 $ morph status Reference mode (git ↔ morph) git HEAD: 16058d326bb5 drift: up to date

One git pull, five new Morph commits, parent chain matches Git's. Each new mirror has morph_origin: "git-hook" (passive, no metrics) and a git_origin_sha tying it to the corresponding teammate commit. From here, you can decide which of those commits deserve evidence (run tests + morph certify) and which stay as bare mirrors.

Rebase pull (git pull --rebase)

The other common case: I have a local commit (me: add b.txt) that hasn't been pushed yet. Teammate pushes three commits to origin/main. I do git pull --rebase so my local commit gets replayed on top of theirs:

$ git pull --rebase From github.com:you/converter fe40765..18503d8 main -> origin/main Rebasing (1/1) Successfully rebased and updated refs/heads/main.

Rebase doesn't fire post-merge — it fires post-rewrite, because the existing local commit gets a new SHA when it's replayed. Morph handles both halves:

  • The three teammate commits are mirrored as fresh git-hook commits, same as the FF case.
  • My local commit's old Morph mirror is annotated with kind: "rewritten", recording the old_git_sha → new_git_sha and the new Morph commit as its successor. The old object stays in the store (immutability is load-bearing) but is no longer on the branch.
  • A new Morph commit replaces it on the branch tip, with parents pointing at the most recent teammate commit.
$ git log --oneline d896641 me: add b.txt # replayed: new sha 18503d8 teammate: a.txt v4 a938d05 teammate: a.txt v3 50e8a16 teammate: a.txt v2 fe40765 initial: a.txt v1 $ morph log --oneline 76d4ab00 me: add b.txt fe138dc8 teammate: a.txt v4 35e558a0 teammate: a.txt v3 49df2265 teammate: a.txt v2 90eadfb8 initial: a.txt v1 $ morph status Reference mode (git ↔ morph) git HEAD: d89664168bde drift: up to date

One thing to call out: if my local commit had a certification (because I'd done morph commit earlier with metrics attached), that certification points at the old Morph hash. The rewritten annotation marks it stale — morph status will surface it as an "uncertified-by-rewrite" commit, and you re-run tests on the rebased commit and use morph certify to re-attach evidence. The merge gate refuses to honor stale certifications, which is exactly the protection you want: rewritten history can't silently inherit evidence it didn't earn.

The takeaway: a teammate doing 50 commits then pushing, followed by you pulling, results in 50 new Morph commits on your machine with the right parent chain — no manual intervention. The post-merge / post-rewrite hooks do the work. You only need to think about drift in the rare cases covered next.

07Commit through Morph when you want evidence

Plain git commit works forever, but the resulting Morph mirror has empty metrics. When you want the commit to carry behavioral evidence — pass rates, test counts, a link back to the Run that produced them — commit through Morph instead. morph commit is a thin wrapper: it shells out to git commit, then attaches metrics to the resulting Morph mirror as a certification annotation.

The pattern is identical to Tutorial 1, but shorter: tell Morph your test suite once with morph config commit.test_command "…", then plain morph commit runs it, parses the metrics, and attaches them to both sides:

# Make a real change: add GBP support, add a test, edit the spec. $ morph config commit.test_command "python3 -m pytest" # once per repo $ git add -A $ morph commit -m "add GBP support" running configured test command: python3 -m pytest attaching evidence from run e19963e9: pass_rate=1, tests_failed=0, tests_passed=3, tests_total=3 [3006006a (cli)] add GBP support git: e7d8df044d8b

Prefer the manual flow? Run morph eval run -- python3 -m pytest first; the breadcrumb it leaves behind is what commit.test_command would have produced, and morph commit picks it up the same way. Use --no-test to skip the auto-run for a single commit (e.g. a quick chore) and --rerun to force a fresh run when the breadcrumb is stale.

Two hashes printed side-by-side: the new Morph commit and the new Git commit it wraps. Both happened in one step. Verify:

$ git log --oneline | head -1 e7d8df0 add GBP support $ morph log --json -n 1 | python3 -c "import json,sys; c=json.load(sys.stdin)['commits'][0]; print(c['short'], '->', c['metrics'])" 3006006a -> {'pass_rate': 1.0, 'tests_failed': 0, 'tests_passed': 3, 'tests_total': 3} $ morph gate PASS: commit 3006006a… satisfies policy

That commit has morph_origin: "cli" (active, not passively mirrored), its certification carries the metrics, and morph gate confirms it satisfies the policy we tightened earlier (required_metrics: [tests_total, tests_passed]). Fresh repos start with an empty required_metrics list so the first commit goes through; opt into enforcement with morph policy init or morph policy require-metrics….

The everyday rhythm: use plain git commit for throwaway in-progress work, and use morph commit when you want the commit to carry evidence — landing a feature, fixing a bug, finishing a refactor. There's no penalty for either: every Git commit becomes a Morph commit. The only difference is whether the Morph side carries metrics.

08When the hooks don't fire — drift

The previous two sections showed the happy paths: git commit fires post-commit, git pull fires post-merge or post-rewrite, and Morph mirrors everything automatically. But a handful of git operations bypass hooks entirely. The realistic ones:

  • git fetch followed by git reset --hard origin/main — resets don't fire any of the four hooks Morph installs.
  • You disabled the hooks (chmod -x .git/hooks/post-merge) or deleted them by accident.
  • You cloned the repo fresh and forgot to run morph init in the new clone — hooks live in .git/hooks/, which Git never tracks, so a fresh clone has none.

When that happens, Morph doesn't blow up — it falls behind Git, and says so. The drift detector walks first-parent ancestry from git HEAD until it finds a mirrored commit, so it catches gaps that span many commits, not just an unmirrored tip.

Simulate it by committing with the hook suppressed via MORPH_INTERNAL=1 (the same escape hatch morph commit uses internally):

$ MORPH_INTERNAL=1 git commit -m "stealth commit (hook suppressed)" --allow-empty $ morph status Reference mode (git ↔ morph) git HEAD: 29352b7b9a36 drift: 3 unmirrored git commits — run `morph reference-sync` last mirrored: 16058d326bb5

(You'd see "1 unmirrored" if you only suppressed one commit; the example shows what real-world drift looks like — multiple commits appearing in one go from a git fetch + git reset.) morph eval gaps --json surfaces the same state as a structured event so agents and CI can detect it programmatically:

$ morph eval gaps --json { "gaps": [ { "kind": "git_morph_drift", "git_head": "29352b7b9a36…", "last_mirrored_git_sha": "16058d326bb5…", "unmirrored_count": 3, "hint": "morph is 3 commit(s) behind git — run `morph reference-sync` …" } ] }

Resolve it with one command, regardless of how many commits are missing:

$ morph reference-sync Synced 3 git commits (HEAD 29352b7b). $ morph status Reference mode (git ↔ morph) git HEAD: 29352b7b9a36 drift: up to date

morph reference-sync walks first-parent ancestry from git HEAD back to the most recent mirrored commit (or the root) and mirrors every commit in the unmirrored span in topo order. It's idempotent — running it twice when already in sync prints "Already up to date." and exits zero. (--backfill is the rarer cousin: it walks from init_at_git_sha instead, and you only need it for late adoption when you missed pre-init history.)

Make this part of your muscle memory: any time you've done something to Git that bypassed the normal commit / pull flow, run morph status. If it shows drift, sync. If you forget, nothing breaks — the next time Morph notices the gap (next status, next eval-gaps, next merge), it'll tell you again.

Other staleness Morph tracks

Drift is the most visible kind of staleness, but Morph tracks four in total. They all surface in morph status; the structured ones also show up in morph eval gaps --json so agents and CI can key off them.

Kind What it means Surfaces
Drift git HEAD has commits that aren't mirrored in morph (covered above). morph status; morph eval gaps as kind: "git_morph_drift"
Stale certifications A morph commit had a kind: "certification" annotation, then git rebase/amend rewrote the underlying git SHA. The certification now describes superseded code. morph status; morph eval gaps as kind: "stale_certifications"
Uncertified git-hook commits Passive mirrors created by post-commit/post-merge with empty observed_metrics. Fine on their own — just means there's no behavioral evidence on those commits yet. morph status only
Merge in progress A morph merge hit a Git conflict; .morph/MERGE_REF.json is the breadcrumb. Resolve with morph merge --continue or --abort. morph status only

Two of those four (drift and stale certifications) emit structured events that an agent or CI job can detect with one command:

$ morph eval gaps --json | jq '.gaps[] | select(.kind == "git_morph_drift" or .kind == "stale_certifications")'

A reasonable habit if you're using Morph day-to-day: alias git pull to git pull && morph status (or whatever shell hook you prefer), and run the morph eval gaps --json filter above in your CI pipeline. The first catches anything you'd miss visually; the second catches anything stuck on someone else's machine that lands in CI.

09Pushing to GitHub

There's no morph push in reference mode. Git is the only thing with a remote, and your remote setup is already done.

$ git push origin main To github.com:you/converter.git d12b9aa..bb95ac4 main -> main

That's it. .morph/ stays local because of the .git/info/exclude entry; only your Git history reaches the remote. A teammate cloning the repo gets the same Git tree they always would, with no idea Morph exists on your machine.

What about sharing behavioral evidence with the team? Stowaway is intentionally local-only. If you want the team to see runs and metrics, two paths: (a) export to CI — have your test pipeline run morph certify --metrics-file … on each merged commit, so metrics live in a shared .morph/ on a server; or (b) flip the whole project to Solo submode when every developer has Morph installed, so plain git merge is gated team-wide. Both are covered in docs/reference-mode.md.

10Recording agent work

The IDE side is identical to Tutorial 1: morph setup cursor (or claude-code / opencode) wires up the MCP server and recording hooks. Those hooks live in .cursor/ (or equivalent) and are independent of the Git hooks reference mode just installed — they don't conflict, and you can install them in any order.

$ morph setup cursor # …writes .cursor/ (MCP, hooks, rules)…

Once an agent has worked on the project in your IDE, the standard inspection commands work exactly as they did in Tutorial 1:

$ morph inspect summary # overview of recorded runs $ morph inspect show <run> # grouped steps (or raw events for a trace hash) $ morph inspect recent # newest traces with phase/scope $ morph serve # browser UI at http://127.0.0.1:8765

Same data, same commands. The only difference from Tutorial 1 is that when you click into a Morph commit in morph serve, it now shows the git_origin_sha so you can cross-reference back to GitHub.

Two niceties on agent-attached commits (since v0.42, still true in v0.48), both visible on the resulting morph show <commit>:
  • morph commit --from-run <run> folds the human author into commit.contributors alongside the agent. You'll see { "id": "...", "role": "human-author" } next to { "id": "claude-opus-4", "role": "primary" } — downstream tooling that filters contributors[?role=='human-author'] can find every commit a person had a hand in, even when the agent did most of the typing.
  • morph commit auto-detects new acceptance cases by diffing the registered eval suite against the parent's, so you don't need to hand-pass --new-cases foo:bar,foo:baz every time you add YAML cases to .morph/evals/. The resulting kind: "introduces_cases" annotation is what morph merge reads when it builds the "introduced on this branch" column of its plan.

11Bringing teammates onto Morph

So far this tutorial has been about you, alone, on a team that uses plain Git. That's the right starting point. But what happens when one of your teammates says "I want what you have", or you decide to move the whole team to Morph as a group? Stowaway is designed to scale up gracefully, and the path has three stages.

Stage 1: stand up a Morph server

So far .morph/ has lived only on your laptop. To share runs, traces, and certifications across machines you need a place those objects can travel to — the same way a Git repo needs a remote. Morph servers are bare repos accessed over SSH; you can stand one up on any box you can ssh into:

# On a server you control (homelab box, EC2 instance, whatever) $ ssh you@homelab $ mkdir -p ~/repos/converter.morph $ morph init --bare ~/repos/converter.morph Initialized bare Morph repository at /home/you/repos/converter.morph

A bare repo has no working tree and no .morph/ wrapper — its files (objects/, refs/, config.json) live directly at the path. That's the only kind of repo you can morph push to from multiple clients.

Now wire your existing reference-mode clone up to it:

$ morph remote add origin you@homelab:repos/converter.morph $ morph push origin main Pushed main -> origin/main (3006006a)

A morph push ships the reachable closure of the new branch tip: every Morph commit object, the eval suites they reference, the runs and traces attached as evidence, the certification annotations — the works. The server runs verify_closure on receive, so a partial push that's missing a referenced object is rejected; either everything lands and the branch ref moves, or nothing does.

Note: morph push is independent of git push. Most reference-mode users alias them back-to-back so a single command pushes both: git push origin main && morph push origin main. The two pushes carry different things — Git ships the file tree, Morph ships the behavioral evidence.

Stage 2: a teammate joins

Your teammate is in their existing Git clone, still on plain Git. They run two commands:

$ morph init . $ morph remote add origin you@homelab:repos/converter.morph $ morph fetch origin origin/main -> 3006006a

That last command is the magic. morph fetch walks the closure on the server side, ships every object the client doesn't already have, and updates remote-tracking refs at refs/remotes/origin/<branch>. After it runs, your teammate has every Morph commit, run, trace, and certification you've ever pushed — locally, in their own .morph/objects/, fully indexable.

From here on, both of you are using Morph. Your teammate's git commits fire their post-commit hook and produce Morph mirrors; your git pulls (now bringing in their work) fire your post-merge hook and mirror their commits on your machine. The behavioral evidence flows through morph push / morph fetch alongside the Git tree.

Stage 3: flip everyone to Solo

Once every committer on the project has Morph installed and you all want the merge gate enforced for plain git merge too — not just for morph merge — flip each clone to Solo submode:

$ morph install-hooks --solo hooks installed: post-commit, post-checkout, post-merge, post-rewrite, pre-merge-commit config: repo_submode = solo

The new fifth hook (pre-merge-commit) is the only difference: it fires before Git records a merge commit, runs the dominance gate against both parents' certified metrics, and exits non-zero if the merged result would regress. Plain git merge is now gated on the same behavioral contract as morph merge. To flip back: morph install-hooks --stowaway.

Submode is local to each clone, recorded in .morph/config.json. There's no team-wide setting, no central authority, no surprise gate that appears for someone who didn't opt in. Each developer makes the flip on their own machine when they're ready.

Honest scope: the evidence horizon

One thing to call out: when your teammate runs morph fetch origin in Stage 2, they receive the closure of refs you've pushed. Local-only branches, uncertified work that hasn't been pushed yet, and any experiments living in .morph/ that you never decided to share won't be there. The merge gate's "no morph claim" rule (a parent without behavioral evidence triggers no violations) means this is operationally fine going forward — but it does mean a regular morph push rhythm is what makes evidence visible to the team. Treat morph push with the same seriousness as git push.

12What you have, what you don't

Stowaway is deliberately scoped. Naming the trade-offs explicitly so there are no surprises:

What you get

Tied-hash mirroring on every Git commit; metric-bearing commits via morph commit; agent runs and traces with full inspection; morph gate as a local CI-style check; drift detection and explicit recovery via morph reference-sync; everything completely local to your clone.

What you don't get (until you opt in)

Team-wide merge gating. Stowaway only protects the merges you perform with morph merge. A teammate doing plain git merge on another machine bypasses it. The fix is the Solo flip described in §11 Stage 3 — once every developer's clone is in Solo submode, plain git merge is gated everywhere too.

One more honest scope note: certifications attached to a Morph commit can become stale if you later git commit --amend or git rebase the underlying Git commit. Morph won't lose the old certification (object immutability is load-bearing), but it'll flag it as stale — both in morph status and in morph eval gaps's structured output as kind: "stale_certifications" — and ask you to re-certify the rewritten commit. That's a feature, not a bug: rewritten history can't silently inherit evidence it didn't earn.

13Where to next

You now have Morph living next to Git in a real project. Every git commit is a behavioral commit, every morph commit carries evidence, and git push works unchanged. From here:

  • docs/MERGE.md and docs/EVAL-DRIVEN.md: behavioral merge gating in action, including what happens when two branches genuinely conflict. morph merge is the canonical merge driver. (A standalone "Tutorial 3" page is on the roadmap; for now those two docs are the authoritative writeup.)
  • docs/reference-mode.md: the comprehensive reference. Covers Solo submode, the pre-merge-commit gate, conflict resolution with morph merge --continue / --abort, stale certifications, recovery, and CI integration.
  • docs/MORPH-AND-GIT.md: the long-form story of running Morph and Git side-by-side — how the two layers cooperate, what crosses the wire on git push vs morph push, and how legacy repos that previously tracked .morph/ in git can recover.
Next · coming soon Branches, merges, and conflicts