recipes
0/8 steps
How it works

How a recipe earns trust.

A recipe is a graph of typed actions run against a real MetaMask build — Extension, Mobile, or Core. It runs while the change is being written, so the work is proven as it is made rather than hoped about and checked at the end. mm-harness is the runner. Everything below, in order.

requires mm-harness 0.34+ ~20 minutes progress saves in this browser

A proposal for how agents can code at MetaMask — offered for teams to try.

Rather hand it to your agent?

The front page carries one prompt that goes further than this page: it sets up the harness and the skills, then drives a real ticket through one.

Why a recipe

Proof, as the change is made. The agent drives a real build, asserts what should be true, and leaves a trace: which actions ran, what they returned, how many times. On-device evidence, produced while the work is happening.

Self-checked in the same context. Replaying a recipe needs no fresh session, no human, and no wait for CI. The agent does it mid-task, in the context it made the change in — so a broken step surfaces while it is still cheap to fix.

Prevents regressions. The recipe outlives the change it was written for. Every later change has to pass it too, so the same bug cannot quietly come back: a one-time check becomes a permanent guard.

Trust. Proven, self-checked, and still proving — so the change is trustworthy while it is being built, not hoped about and verified afterwards. Everything downstream, review included, inherits that.

How it fits your work

You use this through the skills. A skill is the interface — the packaged workflow an agent follows to fix a bug, build a feature, or review a pull request. The skill calls mm-harness underneath, and what the harness runs is a recipe: the executable proof that drives a real build, asserts, and leaves the evidence.

You do not add proof to your work. Working through a skill is what produces it.

Top is how you interact; each layer calls the one below.

1 Skills how you use it — fix a bug, build a feature, review a PR
2 mm-harness what the skill calls underneath
3 Recipe what the harness runs — on-device proof, kept as a regression guard

So what you are adopting is not a testing tool. It is a way of working where the proof comes out of doing the work rather than out of a separate step afterwards.

Skills come from two sources: the public MetaMask/skills repo, and the internal Consensys/skills overlay, which needs access to that org. Both are cloned locally and installed per checkout with yarn skills. You can drive the harness directly — the walkthrough below does, and uses no skill — but skills are the path day to day. The full stack is on Architecture.

Two words

An action is one typed operation: unlock the wallet, press a button, read the account state. A recipe is a JSON graph of actions. The steps below follow discover → drive → prove, which is also the order you will work in from here on. mm-harness is the runner, and it implements a versioned recipe specification — see Architecture.

You never guess capabilities — you discover them. mm-harness actions prints what this checkout can do, and that list is law. The tool refuses an invented action rather than improvising one, which is why its output is worth trusting.

The walkthrough, step by step

Each piece on its own, from an empty terminal to an evidence bundle. Checkboxes persist in this browser, so you can close the tab and come back.

  1. Get the CLI

    Both paths end with mm-harness on your PATH.

    Path A — from zero

    No checkout yet

    Install below, then mm-harness setup-base clones the MetaMask product repos into one standard layout and runs each repo's own dependency install. Use --dry-run first to see the layout it would create.

    available now

    Path B — you already have a checkout

    Install and cd in

    Install globally, then work from inside the checkout. The product, ports, and runtime paths are detected from where you are standing; there is no config file to write.

    available now

    $ npm i -g @deeeed/metamask-harness@latest
    $ mm-harness --version
    Expected
    0.34.x — any 0.34 or newer

    Everything from here runs inside a product checkout:

    $ cd ~/dev/metamask/metamask-extension   # or metamask-mobile, or core
    If it fails

    mm-harness: command not found — your global npm bin is not on PATH. npm prefix -g prints the global prefix; add its bin directory to your shell profile.

    Older than 0.34 — a stale global install is the most common source of "the docs don't match my terminal". Run mm-harness update, or update --check to look without installing.

  2. Run doctor — and learn to read the errors

    A read-only readiness check that launches nothing and names the exact command that fixes each finding.

    $ mm-harness doctor
    Expected — a checkout that needs one repair
    pass extension bridge present manifest=…/extension.action-manifest.json harness: @deeeed/metamask-harness@0.34.x runtime: decision=install (deps-missing) deps=missing webpack=down Dependencies are not installed (no yarn install-state markers). runtime-context: temp/recipe/runtime/agentic-runtime.json (absent — run mm-harness doctor --fix) capture: pass (screenshots: capture-helper → cdp; video: capture-helper) Next: mm-harness launch --adapter extension --target '…'
    The habit the toolchain is built around

    Every failure names one next action. Above, the finding tells you to run doctor --fix and the Next: line says where to go once it passes. Read the message and do what it says — that is also what lets an agent recover on its own instead of guessing.

    Do what it said:

    $ mm-harness doctor --fix
    Expected — the finding is gone
    pass extension bridge present ready=true fixed=[runtime-context] failed=[]

    --fix repairs runtime state the harness owns. It will not launch the app, invent credentials, or pick a wallet fixture; those are your decisions, so it stops and asks.

    If it fails

    deps=missing — run yarn install as usual for that repo. The harness does not manage your product repo's dependencies.

    A finding you do not understandmm-harness doctor --json carries the same findings with structured detail, and is the form to paste to an agent or a bug report.

    Still failing after --fixfixed=[…] failed=[…] separates the two. Anything in failed needs a human decision; the finding says which.

  3. Launch the app under harness control

    Starts the app and its dev server, installing the runtime overlay first if missing. Platform is auto-detected. On iOS, pin one simulator UUID through provisioning and launch so duplicate device names cannot select different simulators.

    $ mm-harness launch --verify          # extension — fullscreen by default
    $ xcrun simctl list devices available # choose one iPhone UUID
    $ mm-harness provision runway ios --device '<UUID>' --force
    $ xcrun simctl get_app_container '<UUID>' io.metamask.MetaMask app
    $ mm-harness launch ios --device '<UUID>' --verify
    $ mm-harness launch android --verify

    Quick relaunch is the default and reuses a healthy runtime. --build is the escape hatch that always clean-builds; reach for it only when native or bundler output changed.

    Core is headless — and says so
    mm-harness launch: core is headless; there is nothing to launch. Next: mm-harness verify

    In core there is no UI to launch, so you get the equivalent step instead of a silent failure. Run mm-harness verify and continue.

    If it fails

    Mobile: more than one device — the harness lists what it found rather than guessing. Re-run with --device <udid|serial>. Prefer the iOS UUID over a name, because Xcode can contain several simulators with the same display name.

    iOS app missing after provisioning — repeat the provision command with --force and the same UUID. Do not use --preflight-mode; it is an internal adapter flag, not a public mm-harness launch option.

    Stuckmm-harness logs tails the dev server and app logs; mm-harness status shows the adapter, devices, and next command.

    Clean slatemm-harness stop stops only what this checkout owns, so parallel checkouts are untouched. Stopping nothing is success.

  4. Discover what this checkout can do

    Prints every typed operation available here, with its fields. The set differs per product: Mobile has UI actions, Core is headless and has none.

    $ mm-harness actions --categories
    Expected — a mobile checkout, with its own counts
    analytics (4) assertion (4) control (4) deeplink (1) evidence (2) perps (11) runtime (4) ui (13) utility (1) wallet (6)

    Categories and counts move with the release and the adapter. Read yours from the command; the shape is the point, not the numbers.

    Then look inside a category, search, or inspect one action's fields:

    $ mm-harness actions --category ui
    $ mm-harness actions positions
    $ mm-harness actions --action read_state

    The discovered list is law. It is the capability boundary, not a subset of some larger API. Call something not on it and the harness refuses, pointing you back at discovery:

    call: unknown action "totally_made_up" for the extension adapter. Next: mm-harness actions --adapter extension --json

    That refusal is why the evidence is worth reading: a tool that invents an action when asked for something impossible would also produce a plausible-looking result, and you would have no way to tell. Add --json when feeding this to an agent.

    If it fails

    Fewer actions than expected — you are seeing only the bundled set. Team libraries add domain actions and must be declared: V3.

    Wrong product detected — pass --adapter mobile|extension|core explicitly, and check you are inside the checkout you meant.

  5. Drive it: read product state

    call runs one action as a one-node recipe through the real engine — same execution path, trace, and evidence as a full run. Start with a read; reads cannot break anything.

    $ mm-harness call read_state
    Core branch

    Core has no wallet UI. Exercise its live controller boundary instead: mm-harness call read_positions mode=all.

    Short names resolve when unambiguous: read_state finds metamask.wallet.read_state. An ambiguous one stops and lists the candidates.

    Expected — shape of the result
    call read_state: pass Result: { "action": "metamask.wallet.read_state", "state": { "selectedAccount": { "name": "Account 1", "address": "0x…" }, "completedOnboarding": true }, "redacted": true }

    Note "redacted": true — wallet state passes through redaction before reaching an artifact, because these bundles get attached to pull requests.

    If it fails

    Runtime health check failed / CDP not reachable — the app is not running. Complete the launch step; mm-harness status confirms what is live.

    Mobile: more than one devicecall fails fast and lists them. Re-run with --device.

    unknown action — not in this adapter's set. mm-harness call --list shows what call accepts here.

  6. Drive it again: UI or headless Core

    Keep the app visible for this one.

    $ mm-harness call navigate page=perps
    Core branch

    Prove a second headless action instead: mm-harness call command cmd="echo core-ready".

    Fields are passed as key=value, or --arg k=v for the same effect.

    Semantic intent, not coordinates

    You asked for a page, not a pixel or a route. The adapter owns the mapping and verifies where it landed, so a drifted route fails loudly instead of quietly doing nothing — which is why these actions survive redesigns. mm-harness actions --action navigate lists accepted values.

    Others worth trying, if your adapter lists them:

    $ mm-harness call press text="Account 1"
    $ mm-harness call screenshot path=proof.png
    If it fails

    Target not found — the element is not on screen. Actions do not hunt blindly; use wait_for when something needs to mount first.

    Core has no UI actions — correct, it is headless. Try mm-harness call command cmd="echo hello" there instead.

  7. Prove it: run your first recipe

    Everything so far was driving by hand. A recipe composes the same operations into a graph that runs identically every time and records what happened. Start with what ships:

    $ mm-harness run --list
    Expected — an extension checkout
    runnable recipes (extension) Inspect: mm-harness run <recipe> --describe perps.clean-market-testnet [metamask] variant=all params=market — Converge one Perps testnet market… perps.lifecycle [metamask] variant=all — Opens a small ETH testnet position, verifies it, closes it… perps.smoke [metamask] variant=extension — Non-mutating Perps domain smoke… runner.action-validation [metamask] variant=extension — Exercises every manifest-declared action… runner.smoke [metamask] variant=extension — Proves the runner executes manifest-declared actions… wallet.smoke [metamask] variant=extension — Fixture-backed proof that the wallet is reachable…

    The bundled set grows between releases; yours is whatever this command prints, not what is printed here.

    Read one first. --describe shows its parameters and every action it composes:

    $ mm-harness run wallet.smoke --describe
    $ mm-harness run wallet.smoke --plan

    --plan validates and prints the execution plan while touching nothing, exiting 5 if the recipe is invalid. Cheap check before the expensive one.

    Now for real, with the evidence somewhere you chose:

    $ mm-harness run wallet.smoke --artifacts-dir ./first-recipe-artifacts
    Expected
    PASS recipe run [extension] summary: PASS status (app.status, 9ms): platform=extension PASS fixture (metamask.wallet.fixture_status, 17ms): path=temp/recipe/runtime/wallet-fixture.json PASS unlock (metamask.wallet.ensure_unlocked, 100ms): proof=extension-unlocked-state PASS state (metamask.wallet.read_state, 94ms): proof=extension-wallet-state PASS screenshot (ui.screenshot, 6.0s): path=screenshots/wallet-smoke.png PASS done (end, 0ms) artifacts (6): Human run report: ./first-recipe-artifacts/report.md Run summary: ./first-recipe-artifacts/summary.json Execution trace: ./first-recipe-artifacts/trace.json Executed recipe: ./first-recipe-artifacts/recipe.json
    Wallet fixtures

    Recipes needing a signed-in wallet read one canonical fixture per checkout; the password comes from the fixture and is never typed. On a missing fixture, mm-harness fixtures shows its status and the safe next command. fixtures init --dev creates a disposable public test wallet that must never hold real funds.

    If it fails

    Exit 5 — validation or trust failure. The recipe is the problem, not your environment; nothing executed. The message names the invalid part.

    Exit 1 — it ran and an action failed. A real result, not a tooling problem: find the first node in trace.json with "ok": false.

    Exit 3 — infrastructure, not your recipe: the app or dev server fell over. Try mm-harness doctor then mm-harness logs.

    Lost trackmm-harness last prints the last significant command with its verdict and evidence paths.

  8. Open the evidence bundle

    The run left a directory behind. This is the part that beats clicking through by hand.

    $ ls first-recipe-artifacts
    $ cat first-recipe-artifacts/summary.json
    $ open first-recipe-artifacts/screenshots
    FileWhat it answers
    summary.jsonDid it pass? How many nodes, how long, which libraries were in play. The verdict.
    trace.jsonWhat happened at every step: per-node verdict, timing, inputs, outputs. Counts come from here.
    recipe.jsonThe graph that actually executed, including each node's stated intent.
    report.mdThe same story in one readable page — the thing to skim first.
    screenshots/Visual evidence captured during the run.
    diagnostics.jsonWarnings and errors the app emitted, kept deliberately separate from the verdict.

    Read them in that order: report.md, then summary.json for the verdict, then trace.json when you want to know why. Cite numbers from trace.json, never from prose written around it.

    If it fails

    Cannot find the directory — without --artifacts-dir it goes to a checkout-local run directory. mm-harness last prints the path.

    No screenshots — capture differs per platform; mm-harness doctor reports it on a capture: line. A degraded fallback still succeeds and records that it degraded in the artifact metadata.


Next

Recipe anatomy

The graph, the intents, the assertions, and the test of whether yours is worth keeping.

Read the anatomy →

Your team's library

Install the skills, declare your library, prove discovery sees it.

Tutorial V3 →

Reviewing someone's proof

A real bundle annotated, including a degraded capture that stayed honest about it.

For reviewers →