Skip to content

feat(skills): install the Forest plugin on Claude Code / Codex, copy the skills for the rest - #814

Open
Gawtier wants to merge 14 commits into
mainfrom
feat/skills-plugin-routes
Open

feat(skills): install the Forest plugin on Claude Code / Codex, copy the skills for the rest#814
Gawtier wants to merge 14 commits into
mainfrom
feat/skills-plugin-routes

Conversation

@Gawtier

@Gawtier Gawtier commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Replaces #804 and #805, which are closed in favour of this one. Their commits are here, rebased, with @PMerlet's review fixes intact — only the distribution design changed, and enough to deserve a diff you can read in one pass rather than a force-push under the old description.

What

forest skills:init / forest skills:update — give a client's coding agent the Forest skills. Two routes, because coding agents split in two families:

route what lands in the repo
Claude Code, Codex we drive their plugin CLI .claude/settings.json (12 lines, committable) — nothing else
Cursor, OpenCode, other we copy SKILL.md files .agents/skills/ (8 skills)

The command asks which agent(s) you use — multi-select, since Claude Code + Cursor is a common pair — pre-checked from repo detection, skippable with a repeatable --agent flag that is required in non-interactive runs (npx, CI).

Why not copy files for everyone (what #804 did)

Claude Code and Codex both have a plugin system with a non-interactive CLI, and both read the same .claude-plugin/marketplace.json — so our existing catalog serves both with no second manifest. Driving it gives us versioning, auto-update, the docs MCP and the plugin's slash commands (/forest:start, /forest:deploy) for free, and copies nothing into the client's repo.

That deletes the whole class of machinery #804 had to hand-roll: the .mcp.json merge and its two preflight validators (.mcp.json is a Claude Code file — Cursor reads .cursor/mcp.json, OpenCode reads opencode.json — and Claude Code now gets the docs MCP from the forest-docs plugin), plus the duplicate .claude/skills/ copy. One skills dir remains, .agents/skills/, which Cursor, OpenCode and Codex all read.

Verified against claude-code 2.1.100 and codex-cli 0.147.0.

Two bugs fixed on the way

Data loss on refresh. skills:update overwrote a user-authored SKILL.md that collided with a bundle path, even though skills:init correctly refused to claim it. previousFiles now bounds what may be overwritten, not just what may be claimed, and kept files are reported. Reproduced end-to-end before and after.

Dangling skill reference, already shipped. The copy route followed a hand-picked list of skill names while the plugin route installs whole plugins. The list dropped deploy-heroku as "internal/test-only" — it is the user-facing production deploy, referenced by onboard (4×), management, boot-standalone-agent and the /forest:deploy command. Every Cursor/OpenCode user had an onboard skill pointing at a skill that was never installed, at the most important step of the flow. There is now one list, FOREST_PLUGINS, read by both routes, and nothing is filtered below the plugin: a plugin is the unit of distribution and is coherent only as a whole. forest-mcp stays out — it is data access, not developer help.

Detection and the prompt

Repo signals (.claude/, CLAUDE.md, .cursor/, .opencode/, …) decide on their own when there is at least one; the PATH is consulted only when the repo is silent — the fresh-project case, where what you have installed is the only signal. Otherwise anyone with several agent CLIs installed gets them all pre-checked in every repo, and confirming the prompt installs plugins they never picked. The "press space" hint lives in the question text, because inquirer's own hint is appended to the first render only and vanishes on the first keypress — exactly when it is needed, since arrow keys move without selecting.

Tested

  • 53 tests on the skills paths. skills:init gains the command-level suite it never had: both routes, their combination, --ref passthrough, missing CLI, partial plugin failure. The service suite covers the plugin-CLI argv per agent, ref pinning per CLI syntax, detection precedence, and the overwrite guard.
  • Full suite: 626 passed, lint clean. (The DB-backed suites need a live Postgres/MySQL/Mongo and are excluded locally, as on main.)
  • End-to-end with the real CLIs, on throwaway repos: plugin route installs forest, forest-code, forest-docs on both agents and writes only .claude/settings.json; copy route lands 8 skills in .agents/skills/; skills:update refreshes both; and the clobber regression is reproduced green.

Note for the reviewer

@PMerlet — your two skills:init fixes and the skills:update hardening are all still in here and still load-bearing: the manifest-claiming guard is what the new overwrite guard extends, and the ref-transition warning is unchanged. What went away is only what the plugin route makes redundant. Sorry for the moving target on the design — the Codex plugin CLI turned out to read our Claude marketplace, which is what made this shape possible.

Note

Add forest skills:init and forest skills:update commands to install Forest skills for coding agents

  • skills:init installs Forest skills via native plugin CLIs for Claude Code and Codex, and copies skill files into .agents/skills/ for Cursor, OpenCode, and others.
  • skills:update re-runs the appropriate install route using a persisted manifest at .forest/skills-manifest.json, overwriting previously managed files and pruning those removed upstream.
  • Both commands merge a delimited Forest information block into CLAUDE.md or AGENTS.md without clobbering user content, and detect installed agents automatically when no --agent flag is provided.
  • The marketplace tarball is fetched from GitHub at a configurable --ref (default main) and extracted using the new tar dependency.
  • Risk: plugin installation uses spawnSync against agent CLIs; missing or incompatible CLIs silently skip that agent rather than aborting.

Macroscope summarized 3dbc585.

Gawtier and others added 12 commits August 17, 2026 18:33
… repo

Project-scoped, marketplace-independent onboarding for the client's coding agent
(Claude Code + Codex). Fetches the public ForestAdmin/ai-marketplace, drops all 8
skill bundles (forest/* + forest-code + forest-legacy) into .claude/skills/ and
.agents/skills/, merges a Forest block into CLAUDE.md/AGENTS.md, and wires the
forest-docs MCP (https://docs.forest.app/mcp, no secret) from the marketplace.

Includes 11 unit tests (skills-manager) and conforms to repo conventions
(array methods, declared `tar` dep, no jest hooks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On the no-force skip path, installSkills reported the bundle-derived file
list as managed even though nothing was written. A user-authored skill dir
that pre-existed the first skills:init (e.g. their own
.claude/skills/layout/SKILL.md) was thus recorded in the manifest, and a
later refresh — once the skill left the upstream bundle — would prune it,
deleting user data.

installSkills now takes the previous manifest's file list (null on a first
run) and, on the skip path, only carries over the intersection of the
bundle-derived list and that previous manifest: files a past run provably
wrote. First run over a pre-existing dir claims nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… write

installDocsMcp runs last (after skill installs, CLAUDE.md/AGENTS.md merges
and stale pruning), so its deliberate throw on an unparseable local
.mcp.json — or a raw ENOENT on a bundle missing forest-docs/.mcp.json —
left a half-applied install with no manifest rewrite.

skills:init now fails fast: validateLocalMcp() runs before any disk
mutation, and validateMarketplaceBundle() checks the fetched bundle right
after the fetch, before the first write. Both are exported from
skills-manager (and unit-tested) so skills:update can reuse them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…i-drift)

Refresh installed skills from the marketplace: overlay the current bundle,
prune Forest-managed files that left it (manifest-scoped, user files kept),
refresh the CLAUDE.md/AGENTS.md block + docs MCP, bump the manifest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two review fixes on the update command, mirroring skills:init:

- Atomicity: run validateLocalMcp() before any disk mutation and
  validateMarketplaceBundle() right after the fetch, so a broken local
  .mcp.json or a bundle without the docs MCP config aborts BEFORE the
  skills/context files are touched — no more half-applied refresh with
  a manifest that lies about it.

- Ref pinning: an install pinned via --ref (tag/SHA) used to jump
  silently to main on the first plain `skills:update`. The update still
  targets the requested ref (default main), but the transition is now
  logged with the exact --ref flag to pass to stay pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Command-level tests (testCli harness, network fetch mocked to a fake
extracted bundle) for the update pipeline:

- no manifest → clear error, non-zero exit, marketplace never fetched;
- unparsable local .mcp.json → fail fast before any fetch/mutation;
- full refresh: managed files overwritten from the bundle, files gone
  upstream pruned, freshly installed files spared (argument order of
  removeStaleSkillFiles), manifest rewritten with the effective ref;
- ref transition logged when the manifest was pinned to another ref;
- --ref passthrough to the fetch and the rewritten manifest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`skills:init` re-implemented by hand what both agents now do natively. The two
distribution families are no longer the same:

- Claude Code and Codex have a plugin system with a non-interactive CLI. Driving
  it (`claude plugin install --scope project`, `codex plugin add`) gives us
  versioning, auto-update, the docs MCP and the plugin's slash commands
  (`/forest:start`) for free, and copies nothing into the client's repo. Both
  read the same `.claude-plugin/marketplace.json`, so one catalog serves both
  (verified against claude-code 2.1.100 and codex-cli 0.147.0).
- Cursor, OpenCode and the other SKILL.md-compatible agents have no such
  mechanism, so file copy stays — but only for them.

What that removes: the `.mcp.json` merge (a Claude Code file, and Claude Code now
gets the docs MCP from the `forest-docs` plugin) along with its two preflight
validators, and the duplicate `.claude/skills/` copy. One skills dir remains,
`.agents/skills/`, which Cursor, OpenCode and Codex all read.

The command now asks which agent(s) the user works with — multi-select, since
Claude Code + Cursor is a common pair — pre-checked from detection, and skippable
with a repeatable `--agent` flag required in non-interactive runs (npx, CI).

Also fixes a data-loss bug on the refresh path: `skills:update` overwrote a
user-authored SKILL.md that collided with a bundle path, even though
`skills:init` had correctly refused to claim it. `previousFiles` now bounds what
may be overwritten, not just what may be claimed, and the kept files are
reported. Reproduced end-to-end before and after.

Tests: `skills:init` gains the command-level suite it never had (both routes,
their combination, --ref passthrough, missing CLI, partial plugin failure);
the service suite covers the plugin-CLI argv per agent, ref pinning, detection
and the new overwrite guard. 48 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ction

detectAgents() treated "the binary answers --version" as equal evidence to a
mark left in the repo. On a machine with several agent CLIs installed — which
is the norm for anyone evaluating them — Claude Code and Codex came pre-checked
in every repo, whatever that repo actually uses. Confirming the prompt then
installed plugins the user never picked, which reads as the multi-select not
letting you choose at all.

Repo signals (.claude/, CLAUDE.md, .cursor/, .cursorrules, .opencode/,
opencode.json, .codex/) now decide on their own when there is at least one. The
PATH is consulted only when the repo says nothing — the fresh-project case
(`npx create-forest`), where what you have installed is the only signal there is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
inquirer appends its own "(Press <space> to select…)" to the FIRST render only:
it vanishes on the first keypress, which is exactly when the user needs it —
arrow keys move the cursor without selecting anything, so a user who starts by
pressing down or enter has no way left to learn that space is the toggle.

Moving the hint into the question text keeps it on screen for every frame,
since inquirer re-renders the message each time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment justified the exclusion by its install-time credential prompt,
which reads as "an opt-in we could add later". That is not the reason.
forest-mcp is a data-access server for querying a live project's records — not
developer help — so it does not belong in a command whose job is to teach the
agent Forest, whatever its auth policy.

No behaviour change: it was already absent from both routes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…plugin route

The copy route followed a hand-picked list of skill names while the plugin route
installs whole plugins. The two disagreed, and the disagreement was not
cosmetic: the list dropped `deploy-heroku`, but `onboard` — which IS copied —
hands the production step to it in four places, as do `management`,
`boot-standalone-agent` and the `/forest:deploy` command. Every Cursor and
OpenCode user therefore had an `onboard` skill pointing at a skill that was
never installed, at the most important step of the flow.

The list also mislabelled `deploy-heroku` as internal/test-only. It is the
user-facing production deploy, and the marketplace treats it as such.

`SKILL_SOURCES` becomes `SKILL_PLUGINS`: the plugins to take skills from, all of
their skills discovered from the bundle. What ships is now decided in one place,
the marketplace, by what a plugin contains — a second definition here could only
drift. Copy route goes from 7 skills to 8, identical to the plugin route.

The two remaining differences are inherent to the formats, not choices: slash
commands and the docs MCP exist only as plugin components, and the copy-route
agents read `skills/` alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-skill filter went in the previous commit; two plugin lists remained,
`SKILL_PLUGINS` for the copy route and `FOREST_PLUGINS` for the plugin route.
Two lists that must agree are one list waiting to disagree — the exact shape of
the bug just fixed, one level up.

`FOREST_PLUGINS` is now the single decision this command makes, applied to both
routes. A plugin without a `skills/` directory is skipped rather than rejected,
because `forest-docs` legitimately carries only an MCP config; the fail-loud
guard moves to what actually signals a broken bundle — copying no skill at all.

What is left is the one filter worth keeping: WHICH plugins ship. That is a
product decision (forest-mcp is data access, not developer help) and plugins are
self-contained, so excluding one breaks nothing. Filtering INSIDE a plugin is
what breaks things: its skills cross-reference each other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/commands/skills/update.ts Outdated
Comment thread src/services/skills/skills-manager.ts Outdated
Comment thread src/commands/skills/init.ts Outdated
Comment thread src/services/skills/skills-manager.ts Outdated
Comment thread src/services/skills/skills-manager.ts Outdated
Comment thread src/services/skills/skills-manager.ts
Comment thread src/services/skills/skills-manager.ts Outdated
Comment thread src/services/skills/skills-manager.ts
Comment thread src/services/skills/skills-manager.ts
Gautier Delache and others added 2 commits August 18, 2026 10:06
- installSkills destroyed a user file sitting at `.agents/skills/<skill>`: it was
  rm -rf'd to make room for the directory, on a first non-`--force` install too.
  A skill is always a directory, so a regular file there is the user's — it is
  now preserved and reported.

- `--force` overwrote colliding files with no previous manifest, because the
  overwrite predicate short-circuited to true when `previousFiles` was null. The
  flag re-writes what a previous run wrote; it does not license destroying files
  we never wrote. `isManaged` is now the only gate. (New files are unaffected —
  the guard only ever sees paths that already exist on disk.)

- AGENTS.md is written by Codex (plugin route) AND Cursor/OpenCode (copy route).
  One block per agent meant the second merge replaced the first — same delimiters
  — so the file described only whichever route ran last. `forestBlock` now takes
  the agents sharing a context file and covers every route feeding it, and
  `contextFileGroups` guarantees one merge per file.

- A plugin agent whose CLI is missing was skipped, yet still got its context file
  and a manifest entry, claiming an install that never happened and sending
  `skills:update` after a plugin that is not there. Only agents actually set up
  are recorded now.

Also: a manifest with no `agents` predates the field, so it can only be a
copy-route install. Reading it as "no agents" refreshed nothing AND rewrote the
manifest without its files, orphaning every skill on disk.

`installSkills` split into a per-skill helper to stay under the complexity rule.
60 tests, one per finding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I only read the first four comments on #814; there were nine. The five missed
ones are all valid, and three are escapes my own guards were written to prevent
but checked one level too shallow.

- copyDir checked the destination for a symlink but never its ANCESTORS, and
  `mkdirSync(..., { recursive: true })` follows them: a symlinked `.agents` or
  `.agents/skills` sent every copied file outside the project. Checked once
  before any write now, so the run aborts whole rather than half-applied.

- listSkillDirs filtered child entries but not the `<plugin>/skills` root itself.
  copyDir's source-symlink check only ever saw real children, so a crafted
  marketplace could symlink that root at any local directory and have its
  contents copied in.

- isWithinSkillDirs required the real parent to sit under the PROJECT root, which
  a skill subdirectory symlinked at `src/` satisfies — so a crafted manifest
  entry pruned `src/index.ts`. Containment is now against the real skills dir,
  which must itself resolve inside the project.

- removeStaleSkillFiles normalized separators for the comparison only: existsSync
  and rmSync still received `.agents\skills\…`, which resolves to nothing on
  Unix, so a manifest written on Windows pruned nothing. It acts on the
  normalized path now (Node accepts forward slashes on Windows too).

- readManifest let a non-array `agents` ({}, a string) through to callers'
  `.filter`. It is normalized to undefined instead of rejecting the manifest,
  since a manifest with no `agents` is a legitimate pre-field copy install.

65 tests, one per finding — including the near-miss the project-root check used
to authorise, and the symlinked-ancestor abort verified end-to-end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants