A browser-automation team for Claude Code. Forge spawns a small team — a driver and a concurrent snippet curator, coordinated by a lead — that drives a real browser, accretes reusable snippets, and (on request) composes verified Playwright specs from the work.
The default mode just does the thing you asked for. Spec mode is opt-in for when a flow is worth pinning into CI. If the team gets genuinely stuck, it escalates back to you with what it tried and what blocked it rather than spinning.
See CHANGELOG.md for the high-level history.
- Node.js — any recent version (tested on 24).
- playwright-cli —
brew install playwright-cli. Forge wraps it.
Supported on macOS and Linux. Windows is currently untested — the wrapper scripts spawn playwright-cli directly and the team lifecycle leans on tmux/pgrep, so expect rough edges there.
-
Install the plugin (one time per machine):
claude plugin marketplace add vivecuervo7/forge claude plugin install forge@vivecuervo7
-
Enable experimental agent teams in
~/.claude/settings.json, then restart Claude Code:{ "env": { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" } } -
Scaffold your project from inside it:
/forge initThis creates
forge/with ahints/directory, a fallback Playwright config, and a self-documenting.gitignore. -
Drive a task. For an unauthenticated site, the bare scaffold is enough:
/forge <describe what you want done>That launches a fresh chromium and goes. For sites with auth or other project-specific behaviour, author hint files in
forge/hints/(seeforge/hints/README.mdfor guidance). Both hint files are optional and additive: fill in only what you need.Want to see forge run end-to-end before adopting it? Try the samples — three project-shaped directories with hints already authored and prompt-by-prompt walkthroughs against public test sites. 5–15 minutes per walkthrough.
| Command | What it does |
|---|---|
/forge init |
Scaffolds the forge/ directory convention into the current project. Idempotent — the starting point for any new project. Offers to draft hints/forge.md from your codebase so you don't start from an empty stub. |
/forge help [topic] |
Quick reference — commands, the mid-run steering phrases, how to watch a drive. |
/forge <task> |
Drive mode. A driver does the task end-to-end while a concurrent curator accretes reusable snippets from novel work. The everyday command. |
/forge spec <task> |
Spec mode. Same two agents — the driver also composes a self-contained .spec.ts from the drive's own trace and verifies it from a cold start against its declared intent — a regression test (assert correct behavior, expect green), a red-green bug repro (assert correct behavior, expect red until the bug is fixed), or an assertion-less scenario (re-run via /forge run). Also fires on natural-language signals — "create a spec for PROJ-123", "write a failing spec that reproduces…", "capture as a spec". |
A forge spec is a re-runnable flow whose assertions each carry an expected outcome; it verifies when the run matches that expectation. The intent is mandatory and explicit (the lead asks if the task is ambiguous), so the driver never has to guess whether a red is success or a defect:
- regression — all hard
expect(...), expected green. The default. - repro — the bug claim is
expect.soft(...)asserting the correct behavior, so the spec is honestly red against the buggy build (the failure is the reproduction) and goes green once the fix lands. Setup assertions stay hard/green. The verifier treats red at the bug claim as success; a green run signals the bug appears fixed → promote the soft claim to a hard regression assertion. Same artifact, red→green across the fix — no annotations, notest.fail(), portable throughnpx playwright testand/forge export. - scenario — no assertions; success = a clean run.
| Command | What it does |
|---|---|
/forge teach <topic> |
A collaborative drive: forge drives a step at a time while you walk it through the quirky bits, and the gotchas you teach (fallbacks, retries, conditional branches) get baked into snippet bodies. Same driver + curator as a normal drive, in collaborative posture — and you can slip into it mid-run ("let me walk you through this next bit") and back out ("you can take it from here"). The deliberate library-building channel for quirks the agent can't be expected to discover. Also fires on "teach forge how to …" / "let me show forge how to …" phrasings. |
| Command | What it does |
|---|---|
/forge run <spec|last|latest> |
Re-runs a verified spec via the standalone runner. No team spawned. |
/forge export <spec-name> |
Exports a composed spec to a self-contained inlined form, suitable for shipping into another test suite. |
Add record as <label> to a run invocation to capture a video at forge/videos/<spec>-<label>.webm. /forge run last spec, record as before → fix the bug → /forge run last spec, record as after produces paired evidence for a PR.
| Command | What it does |
|---|---|
/forge clean [snippets|hints|both] |
Periodic sweep over forge/snippets/ and forge/hints/. Regenerates snippets/INDEX.md, surfaces snippet overlaps, hint sections that should be snippets or scripts, Jira-keyed snippet names, and stale meta. Findings surface via AskUserQuestion — nothing is applied without your approval. |
Try a sample before adopting forge for your own project. Each sample is a project-shaped directory — committed hints, scaffolded config, real-forge-output seed snippets — plus a prompt-by-prompt walkthrough you run yourself.
| Sample | What you'll see |
|---|---|
samples/shop/ |
Authentication, multi-account hint pattern, full spec-mode pipeline with the driver's cold-verify loop, and two browsers running in parallel under different accounts |
samples/internet/ |
Variant-arg parameterisation — one snippet covers a family of probe pages |
samples/widgets/ |
Compositional decomposition — fill + read snippets compose into larger flows |
Start with shop if your work involves any authenticated app — that walkthrough exercises the most surface area. Each sample's README has the exact commands; budget 5–15 minutes per walkthrough.
- Codegen records one flow into one
.spec.ts. Forge accretes snippets across drives and bakes project knowledge into reusable hints. - playwright-cli is the stateless command interface forge wraps. Forge adds the agent team, snippet library, hints, and spec pipeline on top.
- Hand-writing is the maximum-control option. Forge trades some control for automated selector / decomposition / gotcha work, with cold-start verification on the output.
Forge accretes — snippets and hints — and that accretion needs a light touch to stay clean. A few mechanisms keep the library healthy without manual policing:
- Gentle end-of-run hint nudge. When a run keeps tripping over the same thing — a gotcha hit repeatedly, a selector the driver had to dig for — it may flag it, and the lead offers in its wrap-up summary to add a line to
forge.md. At most one suggestion, never a blocking gauntlet; a clean run says nothing. Snippet-level fixes don't go through this — the curator patches snippets directly during the run. /forge clean. The periodic sweep — run it weekly or whenever a session feels like it accreted noise. The cleanup-scan script surfaces snippet overlaps, hint sections that should be snippets or scripts, Jira-keyed snippet names (which date fast), and stale meta. Each finding surfaces viaAskUserQuestion; nothing is applied without your approval./forge clean snippetsalso regeneratesforge/snippets/INDEX.md.- Phase 0 staleness nudge. After any
/forgerun, if the last/forge cleanwas more than 7 days ago, the lead adds a one-line tail nudge to its summary. Non-blocking — you can ignore it and keep working.
Sub-agent discipline supports this from the authoring side. The driver and curator scan forge/snippets/INDEX.md before authoring to avoid overlap. When a spec fails cold verification, the driver fixes the spec body inline or routes the snippet-level fix to the curator (via patch-request) — fixing the cause rather than patching around it. Details live in agents/driver.md and agents/curator.md.
Every snippet in forge/snippets/ carries a structured meta block at the top of the file:
| Field | Required | Purpose |
|---|---|---|
description |
yes | One-line summary — what the snippet does. |
args |
yes | Args object; {} if the snippet takes none. |
tags |
optional | Free-form tags for grouping (e.g. ['auth'], ['checkout', 'angular']). |
flow |
optional | Name of a multi-step flow the snippet belongs to (e.g. shop-checkout). |
phase |
optional | Phase within that flow (e.g. billing→payment). |
requires |
optional | Page / state the snippet expects before invocation. |
enters |
optional | Page / state the snippet leaves the browser in. |
composes |
optional | Names of other snippets this one shells out to. |
supersedes |
optional | Names of older snippets this one replaces. |
forge/snippets/INDEX.md is the auto-generated contract — a grouped table the driver scans before planning a drive (and re-scans before composing a spec) so it knows the library shape without reading every snippet body. It regenerates whenever a snippet is added or modified, and whenever /forge clean snippets runs. Treat it as derived; edit the meta blocks, not the file.
The forge-pw wrapper around playwright-cli accepts --json (or FORGE_JSON=1) and passes through playwright-cli's structured JSON output: {"result": ...} on success, {"isError": true, "error": ...} on failure (check isError, not the exit code). This is mostly for advanced users and the driver agent's internal calls — everyday driving works the same as before. Reach for it when scripting forge into a larger pipeline or when you want machine-parseable failure modes.
Two teammates plus the lead, coordinating via SendMessage. The /forge skill is the team lead — it spawns the teammates, manages the lifecycle (session start, the browser close, shutdown), fields the driver's check-ins (answering from the code or taking them to you), and owns the user channel.
flowchart LR
User([User])
Lead[/"/forge skill<br/>(team lead)"/]
Driver(["forge:driver"])
Curator(["forge:curator"])
Chrome[("chromium<br/>+ playwright-cli")]
Snippets[("forge/snippets/")]
Specs[("forge/specs/")]
User <-->|"ask / report"| Lead
Lead -->|spawn| Driver
Lead -->|spawn| Curator
Driver -.->|"check-in (stuck)"| Lead
Lead -.->|"answer: steer /<br/>read code / ask user"| Driver
Driver -->|"signals: chunk ·<br/>drive-complete · patch-request"| Curator
Curator -.->|"reads verbatim trace"| Driver
Driver -->|"drive / invoke (forge-pw)"| Chrome
Curator -->|"author / patch"| Snippets
Driver -.->|"compose + verify cold<br/><i>spec mode</i>"| Specs
| Agent | Model | Role |
|---|---|---|
forge:driver |
sonnet | Drives the browser via playwright-cli (wrapped by forge-pw) — invoking existing snippets where they match, driving fresh otherwise. In spec mode it also composes a self-contained .spec.ts from its own verbatim trace, verifies it cold, and self-fixes until it matches the declared intent. |
forge:curator |
sonnet | Runs concurrently with the driver: reads the driver's verbatim action-stream (its on-disk transcript, via forge-read-trace) and owns forge/snippets/ — authoring, patching, and splitting snippets as the drive proceeds. |
Both modes run the same two teammates — spec mode doesn't add anyone. The driver signals the curator as it goes (chunk complete per meaningful chunk, drive complete at the end); the curator authors from the verbatim trace, so the library is built from what the driver actually ran, never a paraphrase. In spec mode the verify loop lives inside the driver: it composes from its trace, runs the spec cold, fixes spec-logic inline, and routes snippet-level failures to the curator via patch-request — so a cold-verify fix accretes into the library (fixed once, for every future spec) rather than being worked around per-spec.
When the driver hits something it can't resolve through the browser — a gated control, a value it can't trace, an environment failure — it checks in with the lead rather than reaching outside its lane. The lead is the investigation tier: it reads the app's source/config (read-only) to answer, or takes the question to you. It owns the convergence judgment and is the only one that escalates to the user. Collaborativeness is a dial (autonomous → light-touch → guided → step-by-step): at the low end the driver runs on its own and the lead resolves what it can; turned up, the lead loops you in more readily, and at the top — the /forge teach door — the driver goes step-by-step while you teach it the quirks, which the curator bakes into the snippet bodies.
Token usage: every agent and the /forge skill itself declare model: sonnet in their frontmatter — no Opus tokens are spent on forge work.
Each /forge invocation is stateless. The lead launches a fresh headless chromium with an ephemeral profile — watchable live in the Playwright dashboard (playwright-cli show) rather than a window that steals focus — runs the user's task, and closes it at the end. (Teach mode, an explicit "watch", a headed-preference line in forge/hints/forge.md, or FORGE_HEADED=1 open a visible window instead.) Clean state every time, by design.
For parallel runs against the same project, the constraint is whatever your backend imposes (single-session-per-user is common). Document the constraint in forge.md; the user respects it when launching parallel sessions.
/forge init scaffolds two empty hint files; you fill in what your project needs. Hints are natural-language instructions to the agents, not config — and they split on one boundary, operating the app vs curating the library:
| File | Read by | Holds |
|---|---|---|
forge.md |
The lead + driver | the operate contract — env, accounts, setup/teardown, app structure, routes, selectors, gotchas, spec deviations |
curator.md |
The curator | snippet-authoring conventions (usually empty) |
Both files are optional. Forge drives correctly against the empty scaffold — the defaults cover unauthenticated sites with no special setup. Fill in a hint file only to encode project-specific knowledge the agents can't discover on their own:
forge.md— the high-leverage one. Worth writing once your site has auth, or once you've watched a few drives and noticed the driver enumerating selectors / hitting gotchas the docs could've handed it. The lead reads it too, so a documented gotcha lets it resolve a driver check-in without escalating to you.curator.md— write only if the project's snippet conventions deviate from the curator's defaults.
A project can opt an agent into another file with an in-hint pointer ("the selectors live in selectors.md"); agents follow such pointers, so the strict defaults stay overridable.
Hints don't need to be complete at start. When a run keeps tripping over the same thing — a gotcha, a selector the driver had to dig for — it surfaces a single gentle suggestion at wrap-up: want this in forge.md so the next run handles it from the start? Accept it or ignore it — nothing is forced, and a clean run suggests nothing. Start with the env contract and canonical selectors in forge.md; the rest accretes from real driving. See the shop sample's forge.md for a worked, doc-grounded example of what a mature hint file looks like.
forge.md's optional ## Setup before each run section is for state forge can't reach on its own — server-side data, account provisioning, "wipe the events table" SQL, anything outside the browser. ## Teardown after each run is the symmetric hook for end-of-run cleanup. The lead executes whatever the hint says in plain prose; there's no DSL.
<project>/forge/
├── hints/ # the only tracked content
│ ├── forge.md # scaffolded empty — fill in (lead + driver: env, accounts, app facts)
│ ├── curator.md # scaffolded empty — snippet conventions (curator); usually stays empty
│ └── README.md # init-scaffolded (gitignored) — authoring guidance
├── snippets/ # gitignored — accreted by the curator
├── specs/ # gitignored — composed during spec mode
├── videos/ # gitignored — recordings from /forge run
├── node_modules/ # gitignored — lazy-installed runner deps
├── package.json # gitignored — forge-managed runner manifest
├── playwright.config.ts # init-scaffolded (gitignored) — fallback runner config
├── .gitignore # init-scaffolded (gitignored) — self-ignores; only hints/ tracked
└── README.md # init-scaffolded (gitignored) — explains the layout
/forge init creates the forge/ skeleton — .gitignore, README.md, playwright.config.ts, hints/README.md (the authoring guide), and the two hint files as empty stubs (so the names forge loads are pre-created — fill them in, both optional, empty = defaults). Everything under snippets/, specs/, and videos/ accretes as you drive. Only the hint files are tracked in version control — every other path is local per-machine, regenerated from convention. See the scaffold's inline comments for adapting to projects with their own Playwright runner.
On first spec run (or first snippet invocation), forge lazy-installs its Playwright runner directly into the project's forge/ directory (standard package.json + node_modules/ layout). Self-contained per project, visible in the IDE, removed cleanly by rm -rf forge/ if you ever want to uninstall.
Env handling is delegated to your project. Whatever's in process.env at run time is what your specs and snippets see — direnv, dotenv-cli, manual shell exports, a secrets manager, or the optional dotenv line in the scaffolded forge/playwright.config.ts all work; pick what fits your setup.
Document the loading recipe in forge/hints/forge.md. Don't assume the driver's shell will pick up your .envrc or .env on its own — tell it explicitly how to load values. The driver prepends whatever recipe you put in forge.md to commands that need env values. See samples/shop/forge/hints/forge.md for a worked example using set -a && source .env && set +a.
The driver follows one rule: env values are referenced, never inlined. It uses native shell expansion ($ADMIN_USERNAME) inside its Bash commands; the shell expands at exec time; the tool-call transcript records the unexpanded reference. The rule applies uniformly to every env var — predictable hygiene over per-call judgment.
For projects with multiple test accounts, document the mapping in forge.md in whatever shape fits — a naming convention (admin → $ADMIN_USERNAME / $ADMIN_PASSWORD, user → $USER_USERNAME / $USER_PASSWORD), a provisioning recipe, or anything else. The driver reads the hint and follows it.