diff --git a/.claude/settings.json b/.claude/settings.json
index c0dfef0..705b93f 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -1,8 +1,12 @@
{
- "//": "forgekit dogfoods its own plugin during local dev. These hooks mirror hooks/hooks.json but resolve through ${CLAUDE_PROJECT_DIR} (this repo) instead of ${CLAUDE_PLUGIN_ROOT}, so the guards run without a marketplace install. Every guard is advisory/fail-open; the completion gate honors FORGE_STOPGATE=0.",
+ "//": "forgekit dogfoods its own plugin during local dev. These hooks mirror hooks/hooks.json but resolve through ${CLAUDE_PROJECT_DIR} (this repo) instead of ${CLAUDE_PLUGIN_ROOT}, so the guards run without a marketplace install. Exec form via global/guards/run.mjs: node finds bash (Git Bash on Windows, where bash is usually NOT on PATH) and passes stdin/stdout/exit code through untouched. Every guard is advisory/fail-open; the completion gate honors FORGE_STOPGATE=0.",
"statusLine": {
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/statusline.sh"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/statusline.sh"
+ ]
},
"hooks": {
"SessionStart": [
@@ -11,15 +15,28 @@
"hooks": [
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/recall-load.sh"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/recall-load.sh"
+ ]
},
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh session-start"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh",
+ "session-start"
+ ]
},
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/.claude/hooks/session-start.sh"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-start.sh"
+ ]
}
]
}
@@ -29,11 +46,21 @@
"hooks": [
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh prompt"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh",
+ "prompt"
+ ]
},
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh preflight"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh",
+ "preflight"
+ ]
}
]
}
@@ -44,15 +71,27 @@
"hooks": [
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/protect-paths.sh"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/protect-paths.sh"
+ ]
},
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cost-budget.sh"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/cost-budget.sh"
+ ]
},
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/doom-loop.sh"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/doom-loop.sh"
+ ]
}
]
},
@@ -61,7 +100,12 @@
"hooks": [
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh pre-edit"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh",
+ "pre-edit"
+ ]
}
]
}
@@ -72,11 +116,20 @@
"hooks": [
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/format-on-edit.sh"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/format-on-edit.sh"
+ ]
},
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh capture"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh",
+ "capture"
+ ]
}
]
},
@@ -85,11 +138,20 @@
"hooks": [
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/secret-redact.sh"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/secret-redact.sh"
+ ]
},
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh capture"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh",
+ "capture"
+ ]
}
]
}
@@ -99,15 +161,28 @@
"hooks": [
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/completion-gate.sh"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/completion-gate.sh"
+ ]
},
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/lean-guard.sh"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/lean-guard.sh"
+ ]
},
{
"type": "command",
- "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh stop"
+ "command": "node",
+ "args": [
+ "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs",
+ "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh",
+ "stop"
+ ]
}
]
}
diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json
index 4bef723..a3027b5 100644
--- a/.codex-plugin/plugin.json
+++ b/.codex-plugin/plugin.json
@@ -15,7 +15,7 @@
"interface": {
"displayName": "Forge",
"shortDescription": "Cognitive substrate and one config for every AI coding tool.",
- "longDescription": "Forge adds a pre-action cognitive substrate for coding agents: assumption gating, transparent model routing, impact prediction, scope decomposition, memory/learning context, and verification checklists. It also emits shared rules and MCP config for Codex, Claude Code, Cursor, Gemini, Aider, Copilot, Windsurf/Devin, Zed, Continue, and Roo.",
+ "longDescription": "Forge adds a pre-action cognitive substrate for coding agents: assumption gating, transparent model routing, impact prediction, scope decomposition, memory/learning context, and verification checklists. It also emits shared rules and MCP config for Codex, Claude Code, Cursor, Gemini, Aider, Copilot, Windsurf/Devin, Zed, Continue, OpenClaw, and Roo.",
"developerName": "CodeWithJuber",
"category": "Productivity",
"capabilities": ["MCP", "Skills", "Code Review", "Verification"],
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 171eb68..f583f0a 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -29,7 +29,7 @@ for the full list.
Brand stored as **one token** (the `brand` key in `brand.json`); rebrand = 1 edit.
- **Distributable id = `forgekit`** (npm package + marketplace id) — fixed even if
the brand token changes, so a rename never breaks install.
-- **Scope = full multi-tool day 1** — nine tools plus MCP, from one canonical source.
+- **Scope = full multi-tool day 1** — ten tools plus MCP, from one canonical source.
- **Install = all three channels** (plugin + hardened installer + npm CLI), all
three pointing at the _same_ tree ("one tree, three front doors").
- **Own `lean` + `atlas`** — as _thin layers over proven primitives_, not
@@ -443,7 +443,7 @@ guards through `${CLAUDE_PROJECT_DIR}`.
## Verified cross-tool emit matrix
-_(All rows confirmed against vendor docs.)_ Forge emits config for **nine tools**, plus
+_(All rows confirmed against vendor docs.)_ Forge emits config for **ten tools**, plus
an **MCP server** for Roo Code and VS Code.
| Tool | Native target | How Forge emits |
@@ -457,10 +457,39 @@ an **MCP server** for Roo Code and VS Code.
| **Windsurf/Devin** | `AGENTS.md` auto-discovered; caps 6k/12k chars | Root `AGENTS.md` under caps; detect `.windsurf` vs `.devin` at init |
| **Zed** | first match of a precedence list incl. `AGENTS.md` | Emit `AGENTS.md` + doctor flags any earlier-precedence legacy file shadowing it |
| **Continue** | `.continue/rules/*.md` + `.continue/mcpServers/*.yaml` | Emit a rules file plus the Forge MCP server config |
+| **OpenClaw** | execution-folder `AGENTS.md` as project context; MCP registry is global | Rely on root `AGENTS.md`; write an OpenClaw-shaped `.openclaw/mcp.json` the operator applies with one `openclaw mcp add` |
Roo Code and VS Code receive the Forge MCP server via `forge init`
(`.roo/mcp.json`, `.vscode/mcp.json`) rather than a rules file.
+### OpenClaw: what is automatic and what is not
+
+OpenClaw appends the execution folder's `AGENTS.md` after its configured agent-workspace
+files as project context, so the canonical rules reach it with **no** extra instruction
+file — the same deal as Codex, Cursor and Copilot. Only `AGENTS.md` travels this way:
+OpenClaw deliberately does not load `SOUL.md`, `IDENTITY.md`, `USER.md`, `MEMORY.md` or
+`BOOTSTRAP.md` from the execution folder, so anything Forge wants OpenClaw to read has to
+be inside the canonical body.
+
+For the **config compiler path**, MCP is deliberately not automatic. OpenClaw's server
+registry is `mcp.servers` in the user's global `~/.openclaw/openclaw.json`; Forge never
+writes to another tool's global config. Instead `forge sync` emits a repo-local,
+OpenClaw-shaped fragment at `.openclaw/mcp.json` and reports the exact command that
+registers it:
+
+```bash
+openclaw mcp add forge-cortex --command forge --arg cortex-mcp
+openclaw mcp doctor forge-cortex --probe # prove it starts and lists tools
+```
+
+There is also a separate **bundle installation path**. The published package already ships
+`.codex-plugin/plugin.json`, `global/tools`, and `.mcp.json`; OpenClaw auto-detects that
+layout as a Codex bundle. Installing a trusted local directory or packed archive through
+`openclaw plugins install` loads Forge's skills and bundle-scoped `forge-cortex` MCP server,
+so the manual global registration above is unnecessary for that installation. This does not
+turn Forge's Claude `hooks/hooks.json` automation into OpenClaw guards: only OpenClaw-style
+hook packs execute. Forge therefore provides no ambient pre-action guard on OpenClaw.
+
## Repo layout — one tree, three front doors
```
@@ -552,20 +581,22 @@ from the tree it describes.
```mermaid
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#201a15','primaryTextColor':'#f2ede7','primaryBorderColor':'#372c22','lineColor':'#f26430','secondaryColor':'#272019','tertiaryColor':'#171310','edgeLabelBackground':'#201a15','clusterBkg':'#171310','clusterBorder':'#4a3b2e','fontFamily':'ui-sans-serif, system-ui, sans-serif','fontSize':'14px'},'flowchart':{'curve':'basis','padding':10,'nodeSpacing':36,'rankSpacing':44}}}%%
flowchart LR
- test["test
100 files"]
- src["src
94 files"]
+ test["test
106 files"]
+ src["src
97 files"]
landing["landing
61 files"]
research["research
35 files"]
+ global["global
3 files"]
bench["bench
2 files"]
- global["global
2 files"]
scripts["scripts
2 files"]
docs["docs
1 file"]
examples["examples
1 file"]
- test -- 195 --> src
+ test -- 206 --> src
bench -- 7 --> src
examples -- 4 --> src
test -- 2 --> scripts
scripts --> src
+ src --> global
test --> bench
+ test --> global
```
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c4dd9ea..5b6f09e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,57 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Added
+
+- **OpenClaw is a first-class emit target — the compiler's tenth tool.** Instructions need
+ no new file: OpenClaw appends the execution folder's `AGENTS.md` after its configured
+ agent-workspace files as project context, so the canonical source reaches it the same way
+ it reaches Codex, Cursor and Copilot. MCP is registered explicitly rather than silently:
+ OpenClaw keeps its server registry in the user's global `~/.openclaw/openclaw.json`, which
+ Forge will not write to, so `forge sync` emits an OpenClaw-shaped fragment to
+ `.openclaw/mcp.json` and reports the exact enabling command
+ (`openclaw mcp add forge-cortex --command forge --arg cortex-mcp`). `.openclaw/mcp.json`
+ is an ordinary managed MCP target: idempotent, per-target ownership (a same-name server
+ you wrote yourself is preserved until `--adopt`), and reversible via
+ `forge integrations remove`. `openclaw` is now selectable and auto-detected by
+ `forge tools`. The packaged `.codex-plugin/plugin.json`, `global/tools`, and `.mcp.json`
+ also form an OpenClaw-compatible Codex bundle: installing a trusted checkout or packed
+ archive loads Forge's skills and bundle-scoped MCP server without the config-only path's
+ manual global registration. Forge installs **nothing** into OpenClaw's hook system — there
+ are no ambient guards there, only `AGENTS.md`/skill text and the MCP tools.
+
+### Changed
+
+- **MCP targets address their server bucket by dotted key path.** `emit/mcp.js` resolved a
+ single top-level key (`mcpServers`, `servers`, `context_servers`); OpenClaw nests its
+ registry under `mcp.servers`. The resolver now walks a path, creating missing objects only
+ on write, and refuses to restructure a file where any step already holds a non-object —
+ that shape is the user's and is reported, never rewritten.
+
+### Fixed
+
+- **Claude Code hooks no longer fail on Windows with `spawn bash ENOENT`.** Every Forge hook
+ (the plugin's `hooks/hooks.json`, the `settings.template.json` that `forge init` merges, the
+ statusline) was exec form with `command: "bash"`. Exec-form hooks are spawned directly — no
+ shell, a plain `PATH` lookup — and a default Git for Windows install puts `git.exe` on `PATH`
+ (`Git\cmd`) but not `bash.exe` (`Git\bin`, `Git\usr\bin`), so SessionStart and every other
+ guard died before it ran. Hooks now spawn the zero-dependency launcher
+ `global/guards/run.mjs` (`node run.mjs .sh …`), which resolves bash — `FORGE_BASH`,
+ `CLAUDE_CODE_GIT_BASH_PATH`, the Git install that owns `git` on `PATH`, the standard install
+ dirs, then `PATH` (never WSL's System32 `bash.exe`) — and passes stdin, stdout and the exit
+ code through verbatim, so exit-2 blocks are unchanged. POSIX behaviour is identical (`bash`
+ from `PATH`). `forge init` heals a Forge-owned install left in the old `bash` spelling in
+ place — ownership manifest included, so uninstall still reverses it — while a hand-written
+ hook at a Forge path is left alone; `forge doctor` shows the resolved bash, flags stale hooks
+ (`--fix` re-merges) and requires the launcher as an install asset. Regression tests cover the
+ Windows default-install `PATH` shape, paths with spaces on both OSes, the no-bash failure
+ mode (exit 1 + hint, never a fabricated block) and the packed archive.
+- **`protect-paths` no longer dies (exit 1, fail-open) on machines without `jq`.** Its grep
+ fallback ran under `set -euo pipefail`, so a payload missing `command` (every Write/Edit) or
+ `file_path` (every Bash call) aborted the guard before it could decide — invisible in CI, where
+ `jq` is preinstalled, but the norm on Windows. The fallback now yields an empty field exactly
+ like the `jq` branch, so `.env` writes and destructive `rm` are blocked without `jq`.
+
## [0.32.1] - 2026-08-22
### Fixed
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index de94c9c..25cc35a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,7 +12,7 @@ small and dependency-free is the whole point, so please read this before opening
- **Node.js ESM only.** All code is ES modules (`"type": "module"`). No CommonJS.
- **Supported Node versions:** 20 and 22 (the `>=20` engines floor; Node 18 is EOL).
- **Cross-tool first.** New behavior should work across the tools forgekit targets
- (Claude Code, Codex, Cursor, Gemini, Aider, …), emitted from one source — not
+ (Claude Code, Codex, Cursor, Gemini, Aider, OpenClaw, …), emitted from one source — not
Claude-only. Say so in the PR if a piece is unavoidably tool-specific.
## Getting started
diff --git a/ONBOARDING.md b/ONBOARDING.md
index 590e50c..384d378 100644
--- a/ONBOARDING.md
+++ b/ONBOARDING.md
@@ -5,7 +5,7 @@ context window, wiped every call — so it has no memory of what your team learn
foresight about what an edit breaks, and no enforced guardrails. forgekit is the
**cognitive substrate** that supplies exactly those three things, and it delivers them
as native config to Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed,
-and Continue at once. Author the brain once; every tool reads it.
+Continue, and OpenClaw at once. Author the brain once; every tool reads it.
This page is the fast path: install, configure a repo, do a task, and watch the ledger
start paying off on day two.
@@ -51,9 +51,22 @@ cd ~/your-project
forge init # emits AGENTS.md, CLAUDE.md, .gemini/settings.json, .aider.conf.yml …
```
-Now Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed, and Continue all
-read the **same** rules — each from its own native file (plus MCP server config for Roo
-Code and VS Code).
+Now Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed, Continue, and
+OpenClaw all read the **same** rules — each from its own native file (plus MCP server
+config for Roo Code and VS Code).
+
+On OpenClaw the rules arrive automatically (it reads the execution folder's `AGENTS.md`
+as project context), but the MCP server is a deliberate one-command step, because
+OpenClaw's registry lives in your global `~/.openclaw/openclaw.json` and Forge does not
+write there:
+
+```bash
+openclaw mcp add forge-cortex --command forge --arg cortex-mcp
+openclaw mcp doctor forge-cortex --probe
+```
+
+`forge sync` writes the same definition to `.openclaw/mcp.json` so you can review or
+merge it instead of retyping the flags.
Change a rule later by editing `source/rules.json` (or dropping a per-repo
`.forge/rules.json`), then:
@@ -172,7 +185,7 @@ Forge would rather ship an honest subset with a clear boundary than a vague clai
- **Add a rule** → a bullet in `source/rules.json`, then `forge sync`.
- **Add a tool (skill)** → `global/tools//SKILL.md` with `name` + `description` frontmatter.
-- **Add a guard** → `global/guards/.sh` (source `_guardlib.sh` for fields + the lock), then wire it in `global/settings.template.json` and `hooks/hooks.json`.
+- **Add a guard** → `global/guards/.sh` (source `_guardlib.sh` for fields + the lock), then wire it in `global/settings.template.json` and `hooks/hooks.json` as `node …/guards/run.mjs …/guards/.sh [mode]` — through the portable launcher (bash is not on `PATH` on Windows), never a bare `bash`.
- **Rebrand** → edit `brand.json` (+ `package.json` bin, `.claude-plugin/plugin.json` name).
Every command with worked examples and the full extension guide live in
diff --git a/README.md b/README.md
index ef18ec1..1bec4a4 100644
--- a/README.md
+++ b/README.md
@@ -22,23 +22,29 @@ delivers them into every tool you use.
> content-addressed memory (we call it "proof-carrying memory" / PCM — see the honesty note
> below), heuristic impact foresight, and guardrail hooks (automatic on Claude Code;
> instructions and MCP tools elsewhere) — authored once and delivered as native config to
-> Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed, and Continue (plus MCP
-> config for Roo and VS Code). Guardrails reduce risk; they are not a security sandbox.
+> Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed, Continue, and OpenClaw
+> (plus MCP config for Roo and VS Code). Guardrails reduce risk; they are not a security
+> sandbox.
> **Status: beta — read before you rely on it.**
>
> - The core (`init`, `sync`, `substrate`, `impact`, `ledger`, guards) is tested and in daily
> use; some flags may change before `1.0`.
> - **Claude Code is the deepest-tested integration** (full plugin, ambient `UserPromptSubmit`
-> guards). The other eight tools receive native config plus MCP tools, but have had less
-> real-world exercise.
+> guards). The other nine tools receive native config plus MCP tools, but have had less
+> real-world exercise. On OpenClaw specifically, rules arrive via `AGENTS.md` project
+> context; the config-only path uses a one-command MCP registration, while installing the
+> package as a compatible Codex bundle loads its skills and bundle-scoped MCP server.
+> Neither path provides ambient hooks (see
+> [OpenClaw in ARCHITECTURE](ARCHITECTURE.md#openclaw-what-is-automatic-and-what-is-not)).
> - **Impact/blast-radius analysis is heuristic** — a regex-approximate, conservative code
> graph, not a sound call graph. Treat its output as advisory.
> - **"Proof-carrying memory" is a name, not a formal proof.** Claims are content-addressed and
> carry evidence references; confidence moves only when independent oracles (tests, CI, a
> human) raise it. There is no theorem-prover in the loop.
> - Some integrations shell out — `forge harden`, `forge scan`, and the git-native ledger
-> assume **Bash, Git, and (for a few paths) `jq`** are available.
+> assume **Bash, Git, and (for a few paths) `jq`** are available. Claude hooks on Windows do
+> not require `bash` on `PATH`: their Node launcher finds Git Bash and preserves guard exits.
## Start in 60 seconds
@@ -128,7 +134,7 @@ The day-to-day value first — the substrate gives a frozen model what it can't
(`forge docs sync` sweeps the diff for stale prose, `forge handoff` writes the bounded
session snapshot the next session resumes from, `forge decide` records choices so no
session re-decides them).
-- **One config for 9 tools.** Author your rules once; Forge emits each tool's native config,
+- **One config for 10 tools.** Author your rules once; Forge emits each tool's native config,
plus MCP for Roo and VS Code. Zero runtime dependencies — one Node CLI, plain files in git,
no server.
diff --git a/biome.json b/biome.json
index 1a74a63..90c726b 100644
--- a/biome.json
+++ b/biome.json
@@ -3,7 +3,7 @@
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"files": {
"ignoreUnknown": true,
- "includes": ["src/**", "test/**", "scripts/**", "bench/**"]
+ "includes": ["src/**", "test/**", "scripts/**", "bench/**", "global/guards/run.mjs"]
},
"formatter": {
"enabled": true,
diff --git a/docs/GUIDE.md b/docs/GUIDE.md
index 526b6a6..133ff44 100644
--- a/docs/GUIDE.md
+++ b/docs/GUIDE.md
@@ -704,10 +704,10 @@ tools` fixes that without changing what `sync` emits.
- `forge tools` — show the detected/primary tool (from `.forge/config.json`, else
auto-detected from which agent folder exists — `CLAUDE.md`, `.cursor/`, `.gemini/`,
- `.codex/`, `.zed/`, `.vscode/`, `.aider.conf.yml`, `.continue/`, `.windsurf/`, `.roo/`)
- and which targets are currently gitignored.
+ `.codex/`, `.zed/`, `.vscode/`, `.aider.conf.yml`, `.continue/`, `.windsurf/`, `.roo/`,
+ `.openclaw/`) and which targets are currently gitignored.
- `forge tools ` — record `` (`claude` · `cursor` · `gemini` · `codex` ·
- `zed` · `vscode` · `aider` · `continue` · `windsurf` · `roo`) as this repo's primary tool in
+ `zed` · `vscode` · `aider` · `continue` · `windsurf` · `roo` · `openclaw`) as this repo's primary tool in
`.forge/config.json`, then write a **marked, reversible** block into `.gitignore`
(`# forge:gitignore:begin … # forge:gitignore:end`) that ignores every OTHER tool's
emitted artifacts. Your own `.gitignore` lines are never touched, and the shared
@@ -1209,6 +1209,17 @@ Forge substrate — pre-action advisory (advisory, never blocks):
Nothing to wire — the plugin's [`hooks/hooks.json`](../hooks/hooks.json) installs the
`SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` guards for you.
+**Windows.** Hooks are exec form (`command` + `args`), which Claude Code spawns directly with a
+plain `PATH` lookup and no shell — and a default Git for Windows install puts `git` on `PATH`
+but **not** `bash`. Every hook therefore runs through the portable launcher
+`global/guards/run.mjs` (`node run.mjs .sh [mode]`): `node` is always present, and the
+launcher finds Git Bash itself — `FORGE_BASH`, then `CLAUDE_CODE_GIT_BASH_PATH`, then the Git
+install that owns the `git` on `PATH`, the standard install dirs, and finally a `bash.exe` on
+`PATH` (never WSL's System32 launcher) — and passes stdin, stdout and the exit code through
+untouched, so a guard's exit 2 still blocks. `forge doctor` shows which bash it resolved and
+flags hooks left in the old `bash …` spelling; `forge doctor --fix` (or `forge init`) heals
+Forge-owned ones in place. On macOS/Linux nothing changes: `bash` from `PATH`, as before.
+
Three more ambient layers ride the same hooks:
**Session rehydration (SessionStart).** Besides lessons and the anchored goal, every
@@ -1298,6 +1309,54 @@ emitted `.mcp.json`):
Forge never pretends it can force a hook into a tool that has none — **ambient on Claude
Code, agent-invoked everywhere else.**
+### OpenClaw — emitted config or installable bundle
+
+OpenClaw appends the execution folder's `AGENTS.md` after its configured agent-workspace
+files as project context, so `forge sync` needs no OpenClaw-specific instruction file:
+the canonical rules arrive on their own. Only `AGENTS.md` travels that way — OpenClaw does
+not read `SOUL.md`, `IDENTITY.md`, `USER.md`, `MEMORY.md` or `BOOTSTRAP.md` from the
+execution folder — so keep anything OpenClaw must see inside the canonical body.
+
+When you use only `forge sync`, the MCP server is **not** wired automatically, on purpose.
+OpenClaw keeps its registry in your global `~/.openclaw/openclaw.json` under `mcp.servers`,
+and Forge does not write to another tool's global config. Instead `forge sync` emits the
+OpenClaw-shaped definition to `.openclaw/mcp.json`:
+
+```json
+{ "mcp": { "servers": { "forge-cortex": { "command": "forge", "args": ["cortex-mcp"] } } } }
+```
+
+Register it once, then prove it:
+
+```bash
+openclaw mcp add forge-cortex --command forge --arg cortex-mcp
+openclaw mcp doctor forge-cortex --probe
+```
+
+`.openclaw/mcp.json` is an ordinary managed MCP target: re-running `sync` is idempotent, a
+same-name server you wrote yourself is preserved (claim it with
+`forge integrations add --adopt`), and `forge integrations remove ` reverses
+the add there exactly as it does for every other tool. Removing the entry from the file
+does **not** unregister it inside OpenClaw — use `openclaw mcp unset forge-cortex` for that.
+
+Alternatively, install ForgeKit itself as a compatible bundle. The npm package includes its
+Codex bundle manifest, skills, and MCP definition; OpenClaw maps those to native skills and
+an embedded bundle-scoped MCP server:
+
+```bash
+# Local development checkout
+openclaw plugins install --link . --accept-capabilities
+
+# Or install a trusted archive produced by `npm pack`
+openclaw plugins install ./codewithjuber-forgekit-.tgz --accept-capabilities
+```
+
+Confirm `Format: bundle`, `Bundle format: codex`, and the `forge-cortex` MCP server with
+`openclaw plugins inspect forgekit`. This bundle path does not require the manual global
+`openclaw mcp add` step above. It also does not activate Forge's Claude
+`hooks/hooks.json`: OpenClaw executes only OpenClaw-style hook packs, so Forge still has no
+ambient pre-action guard there.
+
---
## Reading substrate output
@@ -1354,8 +1413,9 @@ picked up by the plugin and by `forge catalog`.
Create `global/guards/.sh` (source `_guardlib.sh` for the shared fields + the
re-entrancy lock), then wire it in `global/settings.template.json` **and**
-[`hooks/hooks.json`](../hooks/hooks.json). Guards must be idempotent and fail-safe —
-worst case they do nothing.
+[`hooks/hooks.json`](../hooks/hooks.json) as `node …/guards/run.mjs …/guards/.sh [mode]`
+— through the portable launcher, never a bare `bash` (not on `PATH` on Windows). Guards must
+be idempotent and fail-safe — worst case they do nothing.
### Add a crew member (sub-agent)
@@ -1426,6 +1486,8 @@ code reads but this table misses fails CI on the forge repo):
| `FORGE_LEDGER_ONLY` | Ledger-only is the DEFAULT (the ledger is the sole store). `0` is the escape hatch — restores the legacy `lessons/*.md` + recall/brain file store while external tooling migrates |
| `FORGE_EMBED` / `FORGE_EMBED_MODEL` / `FORGE_EMBED_TIMEOUT_MS` | optional embeddings tier (ADR-0005) |
| `FORGE_HOME` | override `~/.forge` (recall store location) |
+| `FORGE_BASH` | explicit bash for the hook guards (the `global/guards/run.mjs` launcher). Resolution order: this, `CLAUDE_CODE_GIT_BASH_PATH`, then on Windows the Git for Windows install that owns `git` on `PATH`, the standard install dirs, a non-WSL `bash.exe` on `PATH`; POSIX uses `bash` from `PATH` |
+| `CLAUDE_CODE_GIT_BASH_PATH` | Claude Code's own Git Bash location — honored by the hook launcher when `FORGE_BASH` is unset |
| `FORGE_ROOT` | repo root override for the MCP server |
| `FORGE_AUTHOR` | identity stamped on ledger provenance (defaults to git identity) |
| `FORGE_COST_CEILING` | daily spend (USD) the cost-budget guard warns at (default 10) |
diff --git a/docs/cognitive-substrate/README.md b/docs/cognitive-substrate/README.md
index d1622b5..62e94ae 100644
--- a/docs/cognitive-substrate/README.md
+++ b/docs/cognitive-substrate/README.md
@@ -25,8 +25,8 @@ forge atlas # build the code graph (needed for blast-radius checks)
```
`forge init` configures Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed,
-and Continue (plus MCP config for Roo and VS Code). On Claude Code the check then runs on
-every prompt automatically.
+Continue, and OpenClaw (plus MCP config for Roo and VS Code). On Claude Code the check then
+runs on every prompt automatically.
---
@@ -157,6 +157,13 @@ can call directly. Details + the exact rule wording:
Forge never pretends it can force a hook into a tool that has none — it's ambient on Claude
Code, and agent-invoked everywhere else.
+**OpenClaw** is agent-invoked in exactly this sense. It picks up the rules on its own (the
+execution folder's `AGENTS.md` becomes project context), but its MCP registry is your global
+`~/.openclaw/openclaw.json`, which Forge will not write to. `forge sync` leaves the
+definition in `.openclaw/mcp.json`; you register it once with
+`openclaw mcp add forge-cortex --command forge --arg cortex-mcp`. Forge installs nothing
+into OpenClaw's own hook system.
+
---
## Use it in a script
diff --git a/global/guards/protect-paths.sh b/global/guards/protect-paths.sh
index 630bb6e..edde9d7 100755
--- a/global/guards/protect-paths.sh
+++ b/global/guards/protect-paths.sh
@@ -11,9 +11,9 @@ if command -v jq >/dev/null 2>&1; then
fpath="$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')"
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty')"
else
- tool="$(printf '%s' "$input" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')"
- fpath="$(printf '%s' "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')"
- cmd="$(printf '%s' "$input" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')"
+ tool="$(printf '%s' "$input" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/' || true)"
+ fpath="$(printf '%s' "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/' || true)"
+ cmd="$(printf '%s' "$input" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/' || true)"
fi
deny() {
diff --git a/global/guards/run.mjs b/global/guards/run.mjs
new file mode 100644
index 0000000..b7dd4dd
--- /dev/null
+++ b/global/guards/run.mjs
@@ -0,0 +1,153 @@
+#!/usr/bin/env node
+// Forge hook launcher — runs a bash guard from an exec-form Claude Code hook on every OS.
+//
+// Why this exists: exec-form hooks (`command` + `args`) are spawned directly — no shell, just a
+// plain PATH lookup of `command`. Forge's hooks were `command: "bash"`. On Windows, Git for Windows
+// puts `git.exe` on PATH (`Git\cmd`) but NOT `bash.exe` (`Git\bin`, `Git\usr\bin`), so every hook
+// — SessionStart first — died with `spawn bash ENOENT` before a guard ever ran. `node` IS a real
+// executable on PATH wherever this package is installed, so hooks now spawn
+// `node run.mjs [args…]` and THIS file finds bash: $FORGE_BASH, Claude Code's own
+// $CLAUDE_CODE_GIT_BASH_PATH, the Git for Windows install that owns the `git` on PATH, the standard
+// install dirs, then PATH itself — skipping the WSL launcher in System32, which is not Git Bash.
+// POSIX is unchanged: `bash` from PATH, exactly as before.
+//
+// It must never weaken a guard: stdin (the hook JSON), stdout (SessionStart context,
+// `updatedToolOutput`) and stderr are inherited untouched, and bash's exit code is returned
+// verbatim — a guard's exit 2 still blocks. With no bash anywhere it prints ONE actionable line and
+// exits 1: the same visible, non-blocking hook error the ENOENT was, minus the mystery. Node
+// built-ins only — a launcher that itself failed to load would be exactly the silent no-op the
+// guards exist to prevent.
+import { spawnSync } from "node:child_process";
+import { existsSync } from "node:fs";
+import { basename, win32 } from "node:path";
+import { fileURLToPath } from "node:url";
+
+export const NO_BASH_HINT =
+ "no bash found to run the hook guards — install Git for Windows (bash ships with it) or point " +
+ "FORGE_BASH (or CLAUDE_CODE_GIT_BASH_PATH) at your bash executable";
+
+/**
+ * Windows dirs that ship a `bash.exe` which is NOT Git Bash (the WSL launcher in System32).
+ * @param {string} dir
+ * @param {NodeJS.ProcessEnv} env
+ */
+function isSystemDir(dir, env) {
+ /** @param {string} p */
+ const norm = (p) => String(p).toLowerCase().replaceAll("/", "\\").replace(/\\+$/, "");
+ const d = norm(dir);
+ return [env.SystemRoot, env.windir, "C:\\Windows"]
+ .filter(Boolean)
+ .some((r) => d === norm(r) || d.startsWith(`${norm(r)}\\`));
+}
+
+/**
+ * Locate the bash that runs the guards. `path` is what to spawn (`null` when nothing usable
+ * exists); `via` says how it was found (doctor prints it). Pure — env, platform and the
+ * filesystem probe are injectable, so the Windows logic is unit-tested on every OS.
+ * @param {{env?: NodeJS.ProcessEnv, platform?: string, exists?: (p: string) => boolean}} [opts]
+ * @returns {{ path: string | null, via: string }}
+ */
+export function resolveBash({
+ env = process.env,
+ platform = process.platform,
+ exists = existsSync,
+} = {}) {
+ // 1. Explicit overrides win on every OS: FORGE_BASH (ours), then CLAUDE_CODE_GIT_BASH_PATH —
+ // Claude Code's own setting for the same problem (portable Git, MSYS2, a custom prefix).
+ /** @type {[string, string | undefined][]} */
+ const overrides = [
+ ["FORGE_BASH", env.FORGE_BASH],
+ ["CLAUDE_CODE_GIT_BASH_PATH", env.CLAUDE_CODE_GIT_BASH_PATH],
+ ];
+ for (const [via, p] of overrides) if (p && exists(p)) return { path: p, via };
+ // 2. POSIX: bash from PATH, exactly as the hooks always did (spawn reports ENOENT if absent).
+ if (platform !== "win32") return { path: "bash", via: "PATH" };
+ // 3. Windows — Windows path semantics regardless of the host running this logic (tests).
+ const P = win32;
+ const pathDirs = String(env.PATH ?? env.Path ?? "")
+ .split(P.delimiter)
+ .filter(Boolean);
+ const seen = new Set();
+ /** @type {string[]} */
+ const candidates = [];
+ /** @param {string} p */
+ const add = (p) => {
+ const k = p.toLowerCase();
+ if (!seen.has(k)) {
+ seen.add(k);
+ candidates.push(p);
+ }
+ };
+ // 3a. The Git for Windows install that owns the `git` on PATH: a default install exposes
+ // `Git\cmd\git.exe` only; its siblings `Git\bin\bash.exe` / `Git\usr\bin\bash.exe` are the
+ // bash the guards need. Also covers `Git\bin` or `Git\mingw64\bin` being the PATH entry.
+ for (const dir of pathDirs) {
+ if (isSystemDir(dir, env) || !exists(P.join(dir, "git.exe"))) continue;
+ const root = P.dirname(dir);
+ for (const r of [root, P.dirname(root)]) {
+ add(P.join(r, "bin", "bash.exe"));
+ add(P.join(r, "usr", "bin", "bash.exe"));
+ }
+ }
+ // 3b. Standard install dirs: system-wide, per-user, scoop.
+ for (const base of [env.ProgramFiles, env.ProgramW6432, env["ProgramFiles(x86)"]]) {
+ if (base) add(P.join(base, "Git", "bin", "bash.exe"));
+ }
+ if (env.LOCALAPPDATA) add(P.join(env.LOCALAPPDATA, "Programs", "Git", "bin", "bash.exe"));
+ if (env.USERPROFILE)
+ add(P.join(env.USERPROFILE, "scoop", "apps", "git", "current", "bin", "bash.exe"));
+ for (const p of candidates) if (exists(p)) return { path: p, via: "git-for-windows" };
+ // 3c. A bash.exe on PATH — but never the WSL launcher in System32.
+ for (const dir of pathDirs) {
+ if (isSystemDir(dir, env)) continue;
+ const p = P.join(dir, "bash.exe");
+ if (exists(p)) return { path: p, via: "PATH" };
+ }
+ return { path: null, via: "none" };
+}
+
+/**
+ * `${CLAUDE_PLUGIN_ROOT}` is substituted as a native `C:\…` string on Windows; Git Bash's
+ * `dirname`/`cd` are happiest with forward slashes. No-op on POSIX.
+ * @param {string} p
+ */
+const toPosix = (p) => String(p).replaceAll("\\", "/");
+
+/**
+ * Spawn `bash