Public: anyone can read this; the crew can change it.

Claude Code adds the forge's plugin in one command the forge prints, with no sparse clone #11116

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The forge now serves a Claude Code marketplace manifest at `/plugins/claude-code/marketplace.json`, and Claude Code gets one install command, printed with this forge's own origin. The gate, `tsc`, design-lint and the six plugin, card and token-page suites pass locally (53 tests). I did not run the forge's full check, and I did not try the command in a real Claude Code.

shipped production is serving it deploy befa93c

opened this

The forge now serves a Claude Code marketplace manifest at /plugins/claude-code/marketplace.json, and Claude Code gets one install command, printed with this forge's own origin. The gate, tsc, design-lint and the six plugin, card and token-page suites pass locally (53 tests). I did not run the forge's full check, and I did not try the command in a real Claude Code.

The command:

claude plugin marketplace add <origin>/plugins/claude-code/marketplace.json && claude plugin install forge@forge
  • What I used for the manifest: the Claude Code marketplace format (name, owner, plugins[]). A marketplace served by URL can't carry files, so the plugin entry uses a git-subdir source. Claude Code fetches plugins/claude-code from the forge's public source repository itself, so there's no sparse clone to type. I wrote this from memory of the marketplace docs and haven't checked it against them.
  • Manifest contents: the origin is the one the request came in on. commands lists each .md file under plugins/claude-code/commands, read when the first request arrives, as the OpenCode route does.
  • Where it's printed: the install line in HARNESS_PLUGINS is now this one command, so llms.txt and the MCP card show it. The /settings/tokens Claude Code block shows it as a single "Install the plugin (one command)" field.
  • README: plugins/claude-code/README.md leads with the command and keeps the in-Claude /plugin marketplace add …git route and --plugin-dir as alternatives.
  • Tests: a new plugin-routes.test.ts case fetches the served manifest. It asserts the plugin name, the git-subdir source with a .git URL, and that each command file is listed. I also updated the card and connect-page assertions to the new line.
  • Left out: the manifest is 404 when no public source repository is offered, the same condition that hides the other Claude Code lines. The terminal screen still shows the old two-line marketplace form. The existing root .claude-plugin/marketplace.json, which the git route needs, is unchanged. No new dependency.
  • To try: open /settings/tokens, or curl <origin>/plugins/claude-code/marketplace.json, then run the printed line.
  • Next step: run that command against a preview in a real Claude Code, since I haven't.

plugin_routes.ts and mcp_card.ts now import each other. This is safe because the install lines are lazy arrow functions.

The push hook wanted a "Try:" commit, so there is an empty commit for that.

Closes #11090

commented

The forge now serves a Claude Code marketplace manifest at /plugins/claude-code/marketplace.json, and Claude Code gets one install command, printed with this forge's own origin. The gate, tsc, design-lint and the six plugin, card and token-page suites pass locally (53 tests). I did not run the forge's full check, and I did not try the command in a real Claude Code.

The command:

claude plugin marketplace add <origin>/plugins/claude-code/marketplace.json && claude plugin install forge@forge
  • What I used for the manifest: the Claude Code marketplace format (name, owner, plugins[]). A marketplace served by URL can't carry files, so the plugin entry uses a git-subdir source. Claude Code fetches plugins/claude-code from the forge's public source repository itself, so there's no sparse clone to type. I wrote this from memory of the marketplace docs and haven't checked it against them.
  • Manifest contents: the origin is the one the request came in on. commands lists each .md file under plugins/claude-code/commands, read when the first request arrives, as the OpenCode route does.
  • Where it's printed: the install line in HARNESS_PLUGINS is now this one command, so llms.txt and the MCP card show it. The /settings/tokens Claude Code block shows it as a single "Install the plugin (one command)" field.
  • README: plugins/claude-code/README.md leads with the command and keeps the in-Claude /plugin marketplace add …git route and --plugin-dir as alternatives.
  • Tests: a new plugin-routes.test.ts case fetches the served manifest. It asserts the plugin name, the git-subdir source with a .git URL, and that each command file is listed. I also updated the card and connect-page assertions to the new line.
  • Left out: the manifest is 404 when no public source repository is offered, the same condition that hides the other Claude Code lines. The terminal screen still shows the old two-line marketplace form. The existing root .claude-plugin/marketplace.json, which the git route needs, is unchanged. No new dependency.
  • To try: open /settings/tokens, or curl <origin>/plugins/claude-code/marketplace.json, then run the printed line.
  • Next step: run that command against a preview in a real Claude Code, since I haven't.

plugin_routes.ts and mcp_card.ts now import each other. This is safe because the install lines are lazy arrow functions.

The push hook wanted a "Try:" commit, so there is an empty commit for that.

Changes requested — before the latest push

The manifest serves and the command prints in llms.txt, the card and /settings/tokens, but the change leaves the terminal and docs behind.

Fix first: both doors. The terminal screen in src/tui/ still prints the old two-line marketplace form (the pull says so). The page and the terminal must show the same single command. Move the command to a shared helper if the screen needs it.

Also:

  • docs/features/tokens-harness.md (lines ~26-27) still gives the old /plugin marketplace add ...git + /plugin install how-to. Update it to the one command; the how-to lives there.
  • src/http/plugin_routes.ts: try { readdirSync } catch {} swallows every error silently. Catch only ENOENT, or let the 404 path say why. The commands are also read on every request. Read them once, as the OpenCode route does.
  • The manifest url and metadata.description use ctx.url.origin, which comes from the Host header, and the response is cache-control: public. Use the configured forge URL (app.config.url), as the account page does, so a forged Host cannot poison a shared cache.
  • Add a test that the manifest is 404 when no source repo is offered, and a test for the terminal screen.
  • The pull says the format was written from memory and not checked against Claude Code docs or run. Check git-subdir and the commands field against the docs and say so in the description.
  • mcp_card.ts and plugin_routes.ts now import each other. Put claudeInstall/claudeMarketplace in a small module (e.g. src/core/claude_plugin.ts) so there is no cycle.
  • The now-unused market.add/market.install from the marketplace-lines helper in mcp_card.ts: remove them if nothing else reads them.

Design: the account change is one copyField, the existing component; no design principle broken.

commented

Claude Code now installs the forge plugin with one command: claude plugin marketplace add <forge>/plugins/claude-code/marketplace.json && claude plugin install forge@forge. llms.txt, the MCP card, /settings/tokens and the terminal all print it.

I ran tsc, scripts/gate.ts, design-lint and the tests that cover this change; all pass (62 tests).

Review feedback addressed

  • Terminal: the new-token screen in src/tui/screens/signins.ts prints that same single command, wrapped whole. c copies it, replacing the old a and i keys. Its test checks the wrapped text and the copy output.
  • Shared module: claudeInstall, claudeMarketplace, opencodeInstall and pluginSource now live in src/core/plugin_install.ts, which imports nothing from the forge. The mcp_card.ts ↔ plugin_routes.ts import cycle is gone, and marketplaceLines is removed.
  • Manifest handler: marketplaceManifest(dir, upstream) in src/http/plugin_routes.ts reads the commands once, when the handler is made, and catches only ENOENT.
  • Configured address: the manifest uses publicOrigin(app.config.url, …), not the Host header, so a forged Host cannot poison the cache.
  • Tests: the manifest is 404 with no source repo or no commands directory, and a hostile Host does not appear in the output. The old marketplace-line tests now expect the one command.
  • Docs: docs/features/tokens-harness.md, docs/MCP.md and test/plugins.test.ts describe the one command.

Fixed beyond the feedback: the Dockerfile did not copy plugins/claude-code/commands, so the manifest would have been 404 in production. It now does.

Not verified: I could not reach the Claude Code docs from this sandbox. The git-subdir source and the commands field in the manifest are still written from memory and have not been checked against the docs or run in Claude Code. The format notes at the top of plugin_install.ts record what I believe the docs say. A person should confirm both before merge.

Left out: the in-Claude /plugin marketplace add …git form still appears in the Claude Code plugin README as an alternative.

To try it: open /settings/tokens and copy the install command, or fetch /plugins/claude-code/marketplace.json.

🤖 Generated with Claude Code

Approved

Earlier asks are met: the terminal prints the same one command (c copies it), the shared leaf module removes the import cycle, the manifest uses the configured address rather than the Host header and the Dockerfile ships the commands. On the preview the manifest answers 200 as JSON with nosniff, and llms.txt prints the command with the preview origin. Non-blocking: the git-subdir source and commands field were written from memory and not run in a real Claude Code, so a person should try the command once before release.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs