feat(cc-config): manage plugins, marketplaces, hooks and settings from the dashboard

The Claude Config page was read-only for these four surfaces. It can now:

- register a marketplace from a pasted GitHub `owner/repo`, http(s) URL or
  absolute path, and install / enable / disable / update / uninstall plugins,
  all delegated to the `claude plugin …` CLI so its registry stays consistent;
- edit hooks and settings.json keys in the user, project and project-local
  layers.

Server:

- `server/lib/cc-plugin-cli.js` wraps the CLI with `execFile` and an explicit
  argv array — never a shell string. Every argument is validated before the
  spawn (shape allowlist per kind, no control characters, no leading `-`, no
  `..`), `CLAUDE_CONFIG_DIR` is pinned to the dashboard's CLAUDE_HOME, stdin is
  closed so a prompt errors instead of hanging, and mutating calls are
  serialized in-process.
- `cc-mutate.patchSettings` merges individual dotted keys into one settings
  layer. It re-reads the file unredacted, so keys the UI never displayed —
  including secret-valued ones — survive byte-for-byte. A payload carrying the
  `<redacted>` placeholder is refused (EREDACTED), as are `__proto__` /
  `prototype` / `constructor` segments; a settings file that does not parse is
  reported (EBADJSON) rather than clobbered. Backup first, then atomic write.
- Six additive routes on /api/cc-config; a non-zero CLI exit is a 422 carrying
  the CLI's own message, a missing binary a 503.

Client: install/add bars, per-card plugin and marketplace actions behind a
two-click confirm, and edit modes for hooks and settings that send only the
keys that changed. New i18n keys in en + vi; the explainer banners that said
these surfaces were read-only now describe what the writes actually do.

Tests: 61 new server tests (CLI argv construction and validation, the settings
key-merge, and the six routes against a fake `claude` on PATH).
This commit is contained in:
2026-08-25 15:58:03 +07:00
parent 03a178a718
commit 008ebf1c5c
12 changed files with 3054 additions and 118 deletions
+62 -8
View File
@@ -217,9 +217,9 @@
},
"explain": {
"hooks": {
"title": "Hooks are read-only here on purpose",
"body": "Hooks live in the `hooks` block of `settings.json` files that Claude Code writes to constantly. Editing them from the dashboard would race with the live CLI and could silently break the dashboard's own ingestion (the `hook-handler.js` entry below).",
"howTo": "Edit them yourself with one of:",
"title": "Editing hooks writes settings.json",
"body": "Hooks live in the `hooks` block of the `settings.json` files Claude Code writes to constantly. Edit saves only the event you changed, backs the file up first, and leaves every other key alone — but a `claude` session already running keeps its old hooks until it restarts. Take care with the dashboard`s own ingestion entries (the `hook-handler.js` commands below).",
"howTo": "You can also edit them with:",
"cmd1": "claude config /hooks",
"cmd1Note": "Interactive hook editor inside Claude Code",
"cmd2": "$EDITOR ~/.claude/settings.json",
@@ -228,9 +228,9 @@
"cmd3Note": "Hand-edit the project-level hooks block"
},
"settings": {
"title": "Settings are read-only here on purpose",
"body": "These JSON files are rewritten by the live `claude` CLI on every model change, theme toggle, plugin enable, etc. A dashboard write would race with the CLI and lose state.",
"howTo": "Edit settings via Claude Code itself:",
"title": "Editing settings merges one key at a time",
"body": "These JSON files are rewritten by the live `claude` CLI on every model change, theme toggle, plugin enable, etc. Saving here re-reads the file and writes only the keys you changed (after a timestamped backup), so a concurrent CLI write loses nothing else — but a running session may not pick the change up until it restarts.",
"howTo": "You can also edit them with:",
"cmd1": "claude config set <key> <value>",
"cmd1Note": "Set an individual key safely",
"cmd2": "/config",
@@ -239,8 +239,8 @@
"cmd3Note": "Hand-edit (only when no `claude` process is running)"
},
"plugins": {
"title": "Plugins are managed by the CLI",
"body": "Plugins are bundles with their own registry - `installed_plugins.json` plus install paths under `plugins/cache/`. The dashboard reads them but defers install/uninstall to `claude plugin …` so the registry stays consistent.",
"title": "Plugins run through the `claude` CLI",
"body": "Plugins are bundles with their own registry — `installed_plugins.json` plus install paths under `plugins/cache/`. The buttons here run the matching `claude plugin …` command on this machine, so the registry stays consistent. Installing a plugin means running third-party code: only add marketplaces you trust.",
"install": "Install a plugin:",
"installCmd": "claude plugin install <marketplace>/<plugin>",
"uninstall": "Uninstall this plugin:",
@@ -255,10 +255,64 @@
"addCmd": "claude mcp add <name> <command> [args…]",
"remove": "Remove this server:",
"removeButton": "Copy remove command"
},
"marketplaces": {
"title": "Marketplaces are where plugins come from",
"body": "A marketplace is a git repo (or local folder) with a `.claude-plugin/marketplace.json` listing the plugins it offers. Adding one registers it in `known_marketplaces.json` and clones it locally; its plugins then appear on the Plugins tab, ready to install. Removing one does not uninstall plugins you already installed from it."
}
},
"snippet": {
"copy": "Copy",
"copied": "Copied"
},
"manage": {
"installTitle": "Install a plugin",
"installPlaceholder": "owner/repo, https://… or plugin@marketplace",
"installButton": "Add",
"installHelp": "Paste a GitHub repo, URL or local path to register its marketplace, or type <plugin>@<marketplace> to install a plugin directly. Runs the `claude plugin` CLI on this machine.",
"install": "Install",
"installed": "Installed {{name}}.",
"availableTitle": "Available from your marketplaces",
"cliUnavailable": "Could not list available plugins: {{message}}",
"working": "Working…",
"enable": "Enable",
"enabled": "Enabled {{name}}.",
"disable": "Disable",
"disabled": "Disabled {{name}}.",
"update": "Update",
"updated": "Updated {{name}}.",
"uninstall": "Uninstall",
"confirmUninstall": "Click again to uninstall",
"uninstalled": "Uninstalled {{name}}.",
"remove": "Remove",
"confirmRemove": "Click again to remove",
"addMarketplaceTitle": "Add a marketplace",
"addMarketplacePlaceholder": "owner/repo, https://github.com/… or /absolute/path",
"addMarketplaceButton": "Add",
"addMarketplaceHelp": "Registers the marketplace with `claude plugin marketplace add`; its plugins then show up on the Plugins tab.",
"marketplaceAdded": "Added marketplace {{name}}.",
"marketplaceUpdated": "Updated marketplace {{name}}.",
"marketplaceRemoved": "Removed marketplace {{name}}.",
"hookEvent": "Event name (e.g. PreToolUse)",
"hookAddEvent": "Add event",
"hookRemoveEvent": "Remove event",
"hookAddEntry": "Add hook",
"hookRemoveEntry": "Remove hook",
"hookBadEvent": "“{{event}}” is not a valid event name.",
"hookDupEvent": "Event {{event}} is listed twice.",
"hookEmptyCommand": "Every hook in {{event}} needs a command.",
"hooksSaved": "Hooks saved to {{file}}.",
"hookUneditable": "Left untouched (not in the editable shape): {{events}}",
"settingsKey": "Key",
"settingsKeyPlaceholder": "key or dotted.path",
"settingsValue": "Value (JSON)",
"settingsAddKey": "Add key",
"settingsRemoveKey": "Remove key",
"settingsEmptyKey": "A key name cannot be empty.",
"settingsDupKey": "Key {{key}} is listed twice.",
"settingsBadJson": "Value of {{key}} is not valid JSON.",
"settingsSaved": "Settings saved to {{file}}.",
"settingsHelp": "Values are JSON: \"opus\", true, 42, [\"a\"], {\"k\":1}. Only the keys you change are written; the rest of the file is left as-is.",
"settingsRedactedKeys": "Not editable here because their values are redacted: {{keys}}"
}
}