From 99682d38086a3e0a2e0682f5d0220e0d14586827 Mon Sep 17 00:00:00 2001 From: Yuki Fujisaki Date: Fri, 9 Oct 2026 11:03:08 +0900 Subject: [PATCH 1/3] fix: address Claude plugin directory review findings Prepare the plugin folder for resubmission to the Claude plugin directory with `plugin` as the plugin path. - Start the MCP server with `node ${CLAUDE_PLUGIN_ROOT}/scripts/bundle.js` in plugin/.mcp.json. The directory blocks inline programs such as `node -e` when the plugin is a subfolder of the repository. Codex does not expand ${CLAUDE_PLUGIN_ROOT}, so it gets its own plugin/.codex-mcp.json that runs the bundle relative to the plugin root. - Name MCP tools in skill allowed-tools with the plugin scope (mcp__plugin_deploygate_deploygate__*). The unscoped names never matched, so the pre-approvals had no effect. - Stop pre-approving build tools (./gradlew, xcodebuild, fastlane) and file-mutating shell commands, which the directory holds as broad shell access. Limit file edits to Edit() rules for CI configs and Gradle build scripts. - Add plugin/README.md for the listing, describing what the plugin runs, sends, and stores, and how bundle.js is built. - Add the listing icon at plugin/.claude-plugin/icon.png. - Add tests that keep these invariants. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 3 +- plugin/.claude-plugin/icon.png | Bin 0 -> 9763 bytes plugin/.codex-mcp.json | 9 +++++ plugin/.codex-plugin/plugin.json | 2 +- plugin/.mcp.json | 6 +-- plugin/README.md | 54 +++++++++++++++++++++++++++ plugin/skills/ci-setup/SKILL.md | 2 +- plugin/skills/deploy/SKILL.md | 2 +- plugin/skills/sdk-setup/SKILL.md | 2 +- plugin/skills/setup/SKILL.md | 2 +- src/__tests__/plugin.test.ts | 60 +++++++++++++++++++++++++----- src/__tests__/skills.test.ts | 61 +++++++++++++++++++++++++++---- 12 files changed, 175 insertions(+), 28 deletions(-) create mode 100644 plugin/.claude-plugin/icon.png create mode 100644 plugin/.codex-mcp.json create mode 100644 plugin/README.md diff --git a/CLAUDE.md b/CLAUDE.md index 73eda4d..97f5b78 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,7 @@ CI runs `npm run build && npm test` on Node 24 on every PR and push to main. A s - `members.ts` — multi-step member management (workspace → project → team → app) - `shared-teams.ts` — workspace-wide shared team management -**Plugin assets** (`plugin/`): Contains the Codex plugin manifest (`plugin/.codex-plugin/plugin.json`), legacy Claude plugin manifest (`plugin/.claude-plugin/plugin.json`), MCP server config (`.mcp.json`), skills (slash commands in `plugin/skills/`), GitHub Actions templates (`plugin/templates/`), and the bundled server script. The `plugin/` subdirectory is the published plugin root. +**Plugin assets** (`plugin/`): Contains the Codex plugin manifest (`plugin/.codex-plugin/plugin.json`), legacy Claude plugin manifest (`plugin/.claude-plugin/plugin.json`), MCP server configs (`.mcp.json` for Claude, which runs the bundle via `${CLAUDE_PLUGIN_ROOT}`; `.codex-mcp.json` for Codex, which does not expand that variable and runs it relative to the plugin root), the Claude plugin directory listing README (`plugin/README.md`) and icon (`plugin/.claude-plugin/icon.png`), skills (slash commands in `plugin/skills/`), GitHub Actions templates (`plugin/templates/`), and the bundled server script. The `plugin/` subdirectory is the published plugin root. **Marketplace config** (`.agents/plugins/marketplace.json`): Top-level pointer to the `plugin/` subdirectory for Codex plugin marketplace registration. @@ -59,4 +59,5 @@ Tests validate structural invariants like version consistency between `package.j - TypeScript strict mode, target ES2022, Node16 module resolution - The build produces both `dist/` (tsc output) and `plugin/scripts/bundle.js` (esbuild single-file bundle); the bundle is what end users run - Authentication uses the device authorization code flow. `login_start` → user approves in browser → `login_wait` stores the token at `~/.config/deploygate/token` (0600). `logout` revokes server-side and deletes the file. +- Skill `allowed-tools` must name MCP tools with the plugin scope (`mcp__plugin_deploygate_deploygate__*`), and must not pre-approve build tools (`./gradlew`, `xcodebuild`, `fastlane`), file-mutating shell commands, or unscoped `Write`/`Edit` (use `Edit()`, which also covers `Write`). The Claude plugin directory holds versions that do; `skills.test.ts` enforces this. - The `members.ts` `add_member` tool orchestrates multiple API calls (workspace → project → team) in a single tool invocation, handling "already exists" gracefully diff --git a/plugin/.claude-plugin/icon.png b/plugin/.claude-plugin/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..40b4185afa9f3e97e0778a42a151773aaa810679 GIT binary patch literal 9763 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 literal 0 HcmV?d00001 diff --git a/plugin/.codex-mcp.json b/plugin/.codex-mcp.json new file mode 100644 index 0000000..65d7ccd --- /dev/null +++ b/plugin/.codex-mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "deploygate": { + "command": "node", + "args": ["./scripts/bundle.js"], + "cwd": "." + } + } +} diff --git a/plugin/.codex-plugin/plugin.json b/plugin/.codex-plugin/plugin.json index 020abe1..2d26e5c 100644 --- a/plugin/.codex-plugin/plugin.json +++ b/plugin/.codex-plugin/plugin.json @@ -20,7 +20,7 @@ "ci-cd" ], "skills": "./skills/", - "mcpServers": "./.mcp.json", + "mcpServers": "./.codex-mcp.json", "interface": { "displayName": "DeployGate", "shortDescription": "Upload mobile apps and manage DeployGate distribution workflows", diff --git a/plugin/.mcp.json b/plugin/.mcp.json index 3d6e411..4a66a67 100644 --- a/plugin/.mcp.json +++ b/plugin/.mcp.json @@ -2,11 +2,7 @@ "mcpServers": { "deploygate": { "command": "node", - "args": [ - "-e", - "const fs=require('node:fs');const path=require('node:path');const roots=[process.env.CODEX_PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT,process.cwd(),path.join(process.cwd(),'plugin')].filter(Boolean);const entry=roots.map((root)=>path.join(root,'scripts','bundle.js')).find(fs.existsSync);if(!entry){throw new Error('DeployGate MCP bundle not found');}import(entry);" - ], - "cwd": "." + "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/bundle.js"] } } } diff --git a/plugin/README.md b/plugin/README.md new file mode 100644 index 0000000..de3e68e --- /dev/null +++ b/plugin/README.md @@ -0,0 +1,54 @@ +# DeployGate + +Distribute iOS and Android test builds with [DeployGate](https://deploygate.com) without leaving your coding agent. The plugin uploads IPA, APK, and AAB builds, creates and updates distribution pages, invites testers, lists iOS device UDIDs, and generates CI/CD workflows that upload every build automatically. + +## Install + +In Claude Code, run: + +```text +/plugin install deploygate --marketplace DeployGate/deploygate-agent-plugin +``` + +On Claude Code versions before 2.1.275, run `/plugin marketplace add DeployGate/deploygate-agent-plugin` first, then `/plugin install deploygate@deploygate-marketplace`. Then run `/deploygate:setup` to start. + +## Skills + +| Skill | What it does | +|---|---| +| `/deploygate:setup` | Guided onboarding: sign in, build and upload your first app, create a distribution page, connect notifications, and register iOS devices | +| `/deploygate:deploy` | Build the current project and upload the binary to DeployGate | +| `/deploygate:ci-setup` | Generate a GitHub Actions workflow, or set up Bitrise, CircleCI, and other CI services, to upload builds and distribute pull requests | +| `/deploygate:sdk-setup` | Add the DeployGate SDK to an Android app for crash reporting and screen capture | + +The skills don't pre-approve build commands such as Gradle, `xcodebuild`, or fastlane, so Claude Code asks for your permission before it runs them, unless your own permission settings already allow them. File edits are pre-approved only for the CI configuration files and Gradle build scripts that the skills set up. + +## Requirements + +- Node.js 20 or later +- A DeployGate account. The setup skill links you to sign-up if you don't have one. + +The plugin runs a local MCP server, so its tools work in Claude Code and Cowork but not in Claude chat on claude.ai. + +## What the plugin runs, sends, and stores + +**Runs locally.** The MCP server is `scripts/bundle.js`, started with `node`. It talks to Claude over stdio and opens no network ports. + +**Sends data only to DeployGate.** Every request goes to `https://deploygate.com` over HTTPS: the app binaries you choose to upload, distribution page settings, member invitations, and requests for account, app, and device information. The plugin contacts no other service and sends no telemetry. + +**Stores one credential.** You sign in with a browser-based device authorization flow (`login_start`, then `login_wait`). DeployGate issues a token for this plugin, and the plugin saves it to `~/.config/deploygate/token` (`%APPDATA%\deploygate\token` on Windows) with `0600` permissions. The token is sent only to `https://deploygate.com`. The `logout` tool revokes it on the server and deletes the file. The plugin reads no other credentials or environment variables that hold secrets. + +**CI templates.** The workflows that `ci-setup` writes refer to secrets such as `DEPLOYGATE_API_TOKEN` that you add in your CI service. They run on your CI provider; the plugin itself never reads them. + +## About `scripts/bundle.js` + +`scripts/bundle.js` is the MCP server and its npm dependencies, mainly `@modelcontextprotocol/sdk` and `zod`, bundled into one unminified file with esbuild. It is built from the TypeScript source in [`src/`](https://github.com/DeployGate/deploygate-agent-plugin/tree/main/src) by `npm ci && npm run bundle`, and CI checks that the committed bundle matches a fresh build of the source before each release. + +## Support + +- Source and issues: https://github.com/DeployGate/deploygate-agent-plugin +- Help: help@deploygate.com + +## License + +MIT diff --git a/plugin/skills/ci-setup/SKILL.md b/plugin/skills/ci-setup/SKILL.md index 996d2bc..1c4ec1a 100644 --- a/plugin/skills/ci-setup/SKILL.md +++ b/plugin/skills/ci-setup/SKILL.md @@ -1,7 +1,7 @@ --- name: ci-setup description: Set up CI/CD integration for automated DeployGate uploads and PR-based distribution -allowed-tools: mcp__deploygate__get_user_info Read Write Edit Glob Bash(which:*) Bash(ls:*) +allowed-tools: mcp__plugin_deploygate_deploygate__get_user_info Read Glob Edit(.github/workflows/**) Edit(bitrise.yml) Edit(.circleci/config.yml) Bash(which:*) Bash(ls:*) --- # DeployGate CI/CD Setup diff --git a/plugin/skills/deploy/SKILL.md b/plugin/skills/deploy/SKILL.md index 031895e..4d7461e 100644 --- a/plugin/skills/deploy/SKILL.md +++ b/plugin/skills/deploy/SKILL.md @@ -1,7 +1,7 @@ --- name: deploy description: Build the current project and upload the app binary to DeployGate -allowed-tools: mcp__deploygate__get_user_info mcp__deploygate__upload_app Bash(./gradlew:*) Bash(fastlane:*) Bash(xcodebuild:*) Bash(git rev-parse:*) Bash(which:*) Read Glob +allowed-tools: mcp__plugin_deploygate_deploygate__get_user_info mcp__plugin_deploygate_deploygate__upload_app Bash(git rev-parse:*) Bash(which:*) Read Glob --- # Build and upload app to DeployGate diff --git a/plugin/skills/sdk-setup/SKILL.md b/plugin/skills/sdk-setup/SKILL.md index d863a1b..0e12ca6 100644 --- a/plugin/skills/sdk-setup/SKILL.md +++ b/plugin/skills/sdk-setup/SKILL.md @@ -1,7 +1,7 @@ --- name: sdk-setup description: Add the DeployGate SDK to your Android app for crash reporting and screen capture -allowed-tools: mcp__deploygate__upload_app Read Edit Glob Bash(./gradlew:*) +allowed-tools: mcp__plugin_deploygate_deploygate__upload_app Read Glob Edit(build.gradle) Edit(build.gradle.kts) Edit(settings.gradle) Edit(settings.gradle.kts) --- # DeployGate SDK Setup diff --git a/plugin/skills/setup/SKILL.md b/plugin/skills/setup/SKILL.md index 669782f..fc2b19f 100644 --- a/plugin/skills/setup/SKILL.md +++ b/plugin/skills/setup/SKILL.md @@ -1,7 +1,7 @@ --- name: setup description: Start DeployGate onboarding — set up app distribution from first upload to team-wide deployment -allowed-tools: mcp__deploygate__* Bash(./gradlew:*) Bash(xcodebuild:*) Bash(fastlane:*) Bash(security find-identity:*) Bash(git rev-parse:*) Bash(which:*) Bash(mkdir:*) Bash(cp:*) Bash(zip:*) Bash(cd:*) Bash(ls:*) Read Glob AskUserQuestion +allowed-tools: mcp__plugin_deploygate_deploygate__* Bash(security find-identity:*) Bash(git rev-parse:*) Bash(which:*) Bash(ls:*) Read Glob AskUserQuestion --- ## Response Language diff --git a/src/__tests__/plugin.test.ts b/src/__tests__/plugin.test.ts index 349da53..7d9ecff 100644 --- a/src/__tests__/plugin.test.ts +++ b/src/__tests__/plugin.test.ts @@ -134,21 +134,17 @@ describe("plugin/.mcp.json", () => { expect(servers.deploygate).toBeDefined(); }); - it("uses a Codex-compatible portable entry point resolver", () => { + // The Claude plugin directory blocks inline programs (`node -e`) and paths + // not written in full from ${CLAUDE_PLUGIN_ROOT} when the plugin lives in a + // subfolder of the repository. + it("runs the bundle by its full path from CLAUDE_PLUGIN_ROOT", () => { const servers = mcp.mcpServers as Record< string, Record >; const dg = servers.deploygate; expect(dg.command).toBe("node"); - const args = dg.args as string[]; - expect(args).toHaveLength(2); - expect(args[0]).toBe("-e"); - expect(args[1]).toContain("CODEX_PLUGIN_ROOT"); - expect(args[1]).toContain("CLAUDE_PLUGIN_ROOT"); - expect(args[1]).toContain("scripts"); - expect(args[1]).toContain("bundle.js"); - expect(dg.cwd).toBe("."); + expect(dg.args).toEqual(["${CLAUDE_PLUGIN_ROOT}/scripts/bundle.js"]); }); it("does not pass DEPLOYGATE_API_TOKEN through env", () => { @@ -160,6 +156,52 @@ describe("plugin/.mcp.json", () => { }); }); +describe("plugin/.codex-mcp.json", () => { + const mcp = loadJson("plugin/.codex-mcp.json"); + + it("is the MCP config the Codex manifest points to", () => { + const plugin = loadJson("plugin/.codex-plugin/plugin.json"); + expect(plugin.mcpServers).toBe("./.codex-mcp.json"); + }); + + // Codex does not expand ${CLAUDE_PLUGIN_ROOT}, so it starts the server from + // the plugin root and runs the bundle by a relative path. + it("runs the bundle relative to the plugin root", () => { + const servers = mcp.mcpServers as Record< + string, + Record + >; + const dg = servers.deploygate; + expect(dg.command).toBe("node"); + expect(dg.args).toEqual(["./scripts/bundle.js"]); + expect(dg.cwd).toBe("."); + expect(dg.env).toBeUndefined(); + }); +}); + +// The Claude plugin directory reads only the plugin folder, so the README it +// lists and the icon have to live inside plugin/. +describe("plugin folder listing assets", () => { + it("has a README of at least 40 words outside code blocks", () => { + const readme = readFileSync(resolve(ROOT, "plugin/README.md"), "utf-8"); + const prose = readme.replace(/```[\s\S]*?```/g, ""); + expect(prose.split(/\s+/).filter(Boolean).length).toBeGreaterThanOrEqual( + 40, + ); + }); + + it("has a square PNG icon between 512 and 2048 px under 2 MB", () => { + const icon = readFileSync(resolve(ROOT, "plugin/.claude-plugin/icon.png")); + expect(icon.subarray(1, 4).toString("ascii")).toBe("PNG"); + const width = icon.readUInt32BE(16); + const height = icon.readUInt32BE(20); + expect(width).toBe(height); + expect(width).toBeGreaterThanOrEqual(512); + expect(width).toBeLessThanOrEqual(2048); + expect(icon.length).toBeLessThan(2 * 1024 * 1024); + }); +}); + describe("plugin/scripts/package.json", () => { // The plugin cache copies only plugin/, so this file is what makes Node // load bundle.js as ESM on versions without module syntax detection diff --git a/src/__tests__/skills.test.ts b/src/__tests__/skills.test.ts index d01844b..25f2276 100644 --- a/src/__tests__/skills.test.ts +++ b/src/__tests__/skills.test.ts @@ -161,27 +161,72 @@ describe("skills allowed-tools frontmatter", () => { it("setup skill pre-approves MCP deploygate tools (wildcard)", () => { const content = loadSkill("plugin/skills/setup/SKILL.md"); - expect(content).toMatch(/allowed-tools:.*mcp__deploygate__\*/); + expect(content).toMatch( + /allowed-tools:.*mcp__plugin_deploygate_deploygate__\*/, + ); }); it("deploy skill pre-approves upload_app and get_user_info", () => { const content = loadSkill("plugin/skills/deploy/SKILL.md"); - expect(content).toContain("mcp__deploygate__upload_app"); - expect(content).toContain("mcp__deploygate__get_user_info"); + expect(content).toContain("mcp__plugin_deploygate_deploygate__upload_app"); + expect(content).toContain( + "mcp__plugin_deploygate_deploygate__get_user_info", + ); }); - it("ci-setup skill pre-approves file-editing tools", () => { + it("ci-setup skill pre-approves edits to CI config files only", () => { const content = loadSkill("plugin/skills/ci-setup/SKILL.md"); - expect(content).toMatch(/allowed-tools:.*\bWrite\b/); - expect(content).toMatch(/allowed-tools:.*\bEdit\b/); + expect(content).toMatch(/allowed-tools:.*Edit\(\.github\/workflows\/\*\*\)/); }); - it("sdk-setup skill pre-approves Edit for build.gradle", () => { + it("sdk-setup skill pre-approves Edit for build.gradle only", () => { const content = loadSkill("plugin/skills/sdk-setup/SKILL.md"); - expect(content).toMatch(/allowed-tools:.*\bEdit\b/); + expect(content).toMatch(/allowed-tools:.*Edit\(build\.gradle\)/); + expect(content).toMatch(/allowed-tools:.*Edit\(build\.gradle\.kts\)/); }); }); +// The Claude plugin directory holds a version for review when a skill +// pre-approves broad access, so allowed-tools must stay narrow. +describe("skills allowed-tools meet Claude plugin directory policy", () => { + const skills = ["setup", "deploy", "ci-setup", "sdk-setup"]; + + function allowedTools(skill: string): string[] { + const content = loadSkill(`plugin/skills/${skill}/SKILL.md`); + const line = content.match(/^allowed-tools:(.*)$/m); + expect(line).not.toBeNull(); + return line![1].trim().split(/\s+/); + } + + for (const skill of skills) { + it(`${skill} scopes MCP tools to the plugin`, () => { + for (const tool of allowedTools(skill).filter((t) => + t.startsWith("mcp__"), + )) { + expect(tool).toMatch(/^mcp__plugin_deploygate_deploygate__/); + } + }); + + it(`${skill} does not pre-approve build tools or file-mutating shell commands`, () => { + for (const tool of allowedTools(skill).filter((t) => + t.startsWith("Bash("), + )) { + expect(tool).not.toMatch( + /^Bash\((\.\/|gradlew|fastlane|xcodebuild|cp|cd|mkdir|zip|rm|mv)\b/, + ); + } + }); + + it(`${skill} does not pre-approve unscoped Write or Edit`, () => { + const tools = allowedTools(skill); + expect(tools).not.toContain("Write"); + expect(tools).not.toContain("Edit"); + // Claude Code never consults path rules on Write; Edit(path) covers it. + expect(tools.some((t) => t.startsWith("Write("))).toBe(false); + }); + } +}); + describe("skills/deploy delegates to setup skill for complex cases", () => { const content = loadSkill("plugin/skills/deploy/SKILL.md"); From b587da90c2640612542e883df6627b8d2baf1910 Mon Sep 17 00:00:00 2001 From: Yuki Fujisaki Date: Fri, 9 Oct 2026 11:12:29 +0900 Subject: [PATCH 2/3] fix: pre-approve only the setup skill's onboarding tools Scoping the MCP tool names turned the setup skill's wildcard, which never matched before, into a pre-approval of every tool the server exposes, including delete_project, delete_keystore, remove_* and update_saml_certificate. Name the Phase 1 onboarding tools instead, and leave tools that invite people or grant app access (add_member, create_shared_team, assign_shared_team_to_app) to a per-call approval. A new test rejects MCP wildcards and destructive or access-granting tools in every skill's allowed-tools. Also describe the token path accurately in both READMEs: it honors XDG_CONFIG_HOME, and the 0600 mode applies on macOS and Linux only. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- README.md | 4 ++-- plugin/README.md | 2 +- plugin/skills/setup/SKILL.md | 2 +- src/__tests__/skills.test.ts | 33 +++++++++++++++++++++++++++++---- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 97f5b78..855b617 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,5 +59,5 @@ Tests validate structural invariants like version consistency between `package.j - TypeScript strict mode, target ES2022, Node16 module resolution - The build produces both `dist/` (tsc output) and `plugin/scripts/bundle.js` (esbuild single-file bundle); the bundle is what end users run - Authentication uses the device authorization code flow. `login_start` → user approves in browser → `login_wait` stores the token at `~/.config/deploygate/token` (0600). `logout` revokes server-side and deletes the file. -- Skill `allowed-tools` must name MCP tools with the plugin scope (`mcp__plugin_deploygate_deploygate__*`), and must not pre-approve build tools (`./gradlew`, `xcodebuild`, `fastlane`), file-mutating shell commands, or unscoped `Write`/`Edit` (use `Edit()`, which also covers `Write`). The Claude plugin directory holds versions that do; `skills.test.ts` enforces this. +- Skill `allowed-tools` must name each MCP tool with the plugin scope (`mcp__plugin_deploygate_deploygate__`), never a wildcard, and must not pre-approve destructive or access-granting MCP tools (`delete_*`, `remove_*`, `update_*`, `add_*`, `assign_*`, ...), build tools (`./gradlew`, `xcodebuild`, `fastlane`), file-mutating shell commands, or unscoped `Write`/`Edit` (use `Edit()`, which also covers `Write`). The Claude plugin directory holds versions that pre-approve broad access; `skills.test.ts` enforces these rules. - The `members.ts` `add_member` tool orchestrates multiple API calls (workspace → project → team) in a single tool invocation, handling "already exists" gracefully diff --git a/README.md b/README.md index e0b372f..8ac8c76 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ The plugin signs you in to DeployGate via a browser-based device authorization c 2. Open the URL in a browser where you are signed in to DeployGate and click approve. 3. The agent calls `login_wait`, which returns your workspace information once you approve. -The issued token is stored at `~/.config/deploygate/token` (on Windows, `%APPDATA%\deploygate\token`) with `0600` permissions and reused across sessions. Run the `logout` tool to revoke it server-side and delete the local file. +The issued token is stored at `$XDG_CONFIG_HOME/deploygate/token`, which is `~/.config/deploygate/token` when `XDG_CONFIG_HOME` isn't set (on Windows, `%APPDATA%\deploygate\token`), and reused across sessions. On macOS and Linux, the file has `0600` permissions; on Windows, it keeps the default access control of `%APPDATA%`. Run the `logout` tool to revoke it server-side and delete the local file. ## Skills @@ -91,7 +91,7 @@ Claude Code invokes plugin skills as slash commands. Codex invokes plugin skills | Tool | Description | |---|---| | `login_start` | Begin a browser-based device authorization login. Returns a URL for the user to open and approve. | -| `login_wait` | Poll until the user approves `login_start`. On success, persists the token to `~/.config/deploygate/token` (0600). | +| `login_wait` | Poll until the user approves `login_start`. On success, persists the token to the token file described in [Authentication](#authentication). | | `logout` | Revoke the stored token on the server and delete the local token file. | | `get_user_info` | Get current user information (workspace names, projects). Auto-clears the local token on a 401 response. | diff --git a/plugin/README.md b/plugin/README.md index de3e68e..f62b35e 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -36,7 +36,7 @@ The plugin runs a local MCP server, so its tools work in Claude Code and Cowork **Sends data only to DeployGate.** Every request goes to `https://deploygate.com` over HTTPS: the app binaries you choose to upload, distribution page settings, member invitations, and requests for account, app, and device information. The plugin contacts no other service and sends no telemetry. -**Stores one credential.** You sign in with a browser-based device authorization flow (`login_start`, then `login_wait`). DeployGate issues a token for this plugin, and the plugin saves it to `~/.config/deploygate/token` (`%APPDATA%\deploygate\token` on Windows) with `0600` permissions. The token is sent only to `https://deploygate.com`. The `logout` tool revokes it on the server and deletes the file. The plugin reads no other credentials or environment variables that hold secrets. +**Stores one credential.** You sign in with a browser-based device authorization flow (`login_start`, then `login_wait`). DeployGate issues a token for this plugin, and the plugin saves it to `$XDG_CONFIG_HOME/deploygate/token`, which is `~/.config/deploygate/token` when `XDG_CONFIG_HOME` isn't set, or to `%APPDATA%\deploygate\token` on Windows. On macOS and Linux, the file has `0600` permissions, so only your user can read it. On Windows, it keeps the default access control of your `%APPDATA%` folder. The token is sent only to `https://deploygate.com`. The `logout` tool revokes it on the server and deletes the file. The plugin reads no other credentials or environment variables that hold secrets. **CI templates.** The workflows that `ci-setup` writes refer to secrets such as `DEPLOYGATE_API_TOKEN` that you add in your CI service. They run on your CI provider; the plugin itself never reads them. diff --git a/plugin/skills/setup/SKILL.md b/plugin/skills/setup/SKILL.md index fc2b19f..f6dbe1c 100644 --- a/plugin/skills/setup/SKILL.md +++ b/plugin/skills/setup/SKILL.md @@ -1,7 +1,7 @@ --- name: setup description: Start DeployGate onboarding — set up app distribution from first upload to team-wide deployment -allowed-tools: mcp__plugin_deploygate_deploygate__* Bash(security find-identity:*) Bash(git rev-parse:*) Bash(which:*) Bash(ls:*) Read Glob AskUserQuestion +allowed-tools: mcp__plugin_deploygate_deploygate__login_start mcp__plugin_deploygate_deploygate__login_wait mcp__plugin_deploygate_deploygate__get_user_info mcp__plugin_deploygate_deploygate__upload_app mcp__plugin_deploygate_deploygate__create_distribution mcp__plugin_deploygate_deploygate__get_udids mcp__plugin_deploygate_deploygate__get_notification_settings_url Bash(security find-identity:*) Bash(git rev-parse:*) Bash(which:*) Bash(ls:*) Read Glob AskUserQuestion --- ## Response Language diff --git a/src/__tests__/skills.test.ts b/src/__tests__/skills.test.ts index 25f2276..0d82675 100644 --- a/src/__tests__/skills.test.ts +++ b/src/__tests__/skills.test.ts @@ -159,11 +159,21 @@ describe("skills allowed-tools frontmatter", () => { }); } - it("setup skill pre-approves MCP deploygate tools (wildcard)", () => { + it("setup skill pre-approves the Phase 1 onboarding tools", () => { const content = loadSkill("plugin/skills/setup/SKILL.md"); - expect(content).toMatch( - /allowed-tools:.*mcp__plugin_deploygate_deploygate__\*/, - ); + for (const tool of [ + "login_start", + "login_wait", + "get_user_info", + "upload_app", + "create_distribution", + "get_udids", + "get_notification_settings_url", + ]) { + expect(content).toMatch( + new RegExp(`allowed-tools:.*mcp__plugin_deploygate_deploygate__${tool}\\b`), + ); + } }); it("deploy skill pre-approves upload_app and get_user_info", () => { @@ -207,6 +217,21 @@ describe("skills allowed-tools meet Claude plugin directory policy", () => { } }); + // The server also exposes destructive and access-granting tools + // (delete_project, remove_*, update_saml_certificate, add_member, ...), + // so each skill names the tools it pre-approves instead of using a + // wildcard, and leaves those to a per-call approval. + it(`${skill} pre-approves no MCP wildcard and no destructive or access-granting tools`, () => { + for (const tool of allowedTools(skill).filter((t) => + t.startsWith("mcp__"), + )) { + expect(tool).not.toContain("*"); + expect(tool).not.toMatch( + /__(delete|remove|update|protect|unprotect|download|add|assign)_/, + ); + } + }); + it(`${skill} does not pre-approve build tools or file-mutating shell commands`, () => { for (const tool of allowedTools(skill).filter((t) => t.startsWith("Bash("), From a8dfd3ff232b77b0c7b02357f9dfec32facc2231 Mon Sep 17 00:00:00 2001 From: Yuki Fujisaki Date: Fri, 9 Oct 2026 11:28:49 +0900 Subject: [PATCH 3/3] fix: allowlist pre-approvable MCP tools and disclose keystore reads The policy test matched destructive tool names with a pattern, which let create_shared_team through although the setup skill leaves it to a per-call approval. Check every pre-approved MCP tool against an explicit allowlist instead, so any other tool, including ones added to the server later, needs a deliberate change to be pre-approved. The plugin README said the plugin reads no credentials besides its token, but update_keystore reads the keystore file and passwords the user gives it and sends them to DeployGate. Disclose that, and that nothing is stored locally. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- plugin/README.md | 6 ++++-- src/__tests__/skills.test.ts | 25 +++++++++++++++++-------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 855b617..7c5d429 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,5 +59,5 @@ Tests validate structural invariants like version consistency between `package.j - TypeScript strict mode, target ES2022, Node16 module resolution - The build produces both `dist/` (tsc output) and `plugin/scripts/bundle.js` (esbuild single-file bundle); the bundle is what end users run - Authentication uses the device authorization code flow. `login_start` → user approves in browser → `login_wait` stores the token at `~/.config/deploygate/token` (0600). `logout` revokes server-side and deletes the file. -- Skill `allowed-tools` must name each MCP tool with the plugin scope (`mcp__plugin_deploygate_deploygate__`), never a wildcard, and must not pre-approve destructive or access-granting MCP tools (`delete_*`, `remove_*`, `update_*`, `add_*`, `assign_*`, ...), build tools (`./gradlew`, `xcodebuild`, `fastlane`), file-mutating shell commands, or unscoped `Write`/`Edit` (use `Edit()`, which also covers `Write`). The Claude plugin directory holds versions that pre-approve broad access; `skills.test.ts` enforces these rules. +- Skill `allowed-tools` must name each MCP tool with the plugin scope (`mcp__plugin_deploygate_deploygate__`), never a wildcard, and may pre-approve only the non-destructive MCP tools that grant no access listed in `PRE_APPROVABLE_MCP_TOOLS` in `skills.test.ts`; it must not pre-approve build tools (`./gradlew`, `xcodebuild`, `fastlane`), file-mutating shell commands, or unscoped `Write`/`Edit` (use `Edit()`, which also covers `Write`). The Claude plugin directory holds versions that pre-approve broad access; `skills.test.ts` enforces these rules. - The `members.ts` `add_member` tool orchestrates multiple API calls (workspace → project → team) in a single tool invocation, handling "already exists" gracefully diff --git a/plugin/README.md b/plugin/README.md index f62b35e..1cc0a61 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -34,9 +34,11 @@ The plugin runs a local MCP server, so its tools work in Claude Code and Cowork **Runs locally.** The MCP server is `scripts/bundle.js`, started with `node`. It talks to Claude over stdio and opens no network ports. -**Sends data only to DeployGate.** Every request goes to `https://deploygate.com` over HTTPS: the app binaries you choose to upload, distribution page settings, member invitations, and requests for account, app, and device information. The plugin contacts no other service and sends no telemetry. +**Sends data only to DeployGate.** Every request goes to `https://deploygate.com` over HTTPS: the app binaries and Android signing keystores you choose to upload, distribution page settings, member invitations, and requests for account, app, and device information. The plugin contacts no other service and sends no telemetry. -**Stores one credential.** You sign in with a browser-based device authorization flow (`login_start`, then `login_wait`). DeployGate issues a token for this plugin, and the plugin saves it to `$XDG_CONFIG_HOME/deploygate/token`, which is `~/.config/deploygate/token` when `XDG_CONFIG_HOME` isn't set, or to `%APPDATA%\deploygate\token` on Windows. On macOS and Linux, the file has `0600` permissions, so only your user can read it. On Windows, it keeps the default access control of your `%APPDATA%` folder. The token is sent only to `https://deploygate.com`. The `logout` tool revokes it on the server and deletes the file. The plugin reads no other credentials or environment variables that hold secrets. +**Stores one credential.** You sign in with a browser-based device authorization flow (`login_start`, then `login_wait`). DeployGate issues a token for this plugin, and the plugin saves it to `$XDG_CONFIG_HOME/deploygate/token`, which is `~/.config/deploygate/token` when `XDG_CONFIG_HOME` isn't set, or to `%APPDATA%\deploygate\token` on Windows. On macOS and Linux, the file has `0600` permissions, so only your user can read it. On Windows, it keeps the default access control of your `%APPDATA%` folder. The token is sent only to `https://deploygate.com`. The `logout` tool revokes it on the server and deletes the file. + +**Reads other credentials only when you ask.** The `update_keystore` tool reads the Android signing keystore file at the path you give it and takes the keystore and key passwords you provide. It sends them to `https://deploygate.com` to replace the app's signing keystore, and doesn't store them locally. The plugin reads no environment variables that hold secrets. **CI templates.** The workflows that `ci-setup` writes refer to secrets such as `DEPLOYGATE_API_TOKEN` that you add in your CI service. They run on your CI provider; the plugin itself never reads them. diff --git a/src/__tests__/skills.test.ts b/src/__tests__/skills.test.ts index 0d82675..6ef03b7 100644 --- a/src/__tests__/skills.test.ts +++ b/src/__tests__/skills.test.ts @@ -218,17 +218,26 @@ describe("skills allowed-tools meet Claude plugin directory policy", () => { }); // The server also exposes destructive and access-granting tools - // (delete_project, remove_*, update_saml_certificate, add_member, ...), - // so each skill names the tools it pre-approves instead of using a - // wildcard, and leaves those to a per-call approval. - it(`${skill} pre-approves no MCP wildcard and no destructive or access-granting tools`, () => { + // (delete_project, remove_*, update_saml_certificate, add_member, + // create_shared_team, ...). Skills may pre-approve only the tools in + // this list, so a wildcard or any other tool, including one added to + // the server later, needs a per-call approval unless this list is + // deliberately extended. + const PRE_APPROVABLE_MCP_TOOLS = [ + "login_start", + "login_wait", + "get_user_info", + "upload_app", + "create_distribution", + "get_udids", + "get_notification_settings_url", + ].map((tool) => `mcp__plugin_deploygate_deploygate__${tool}`); + + it(`${skill} pre-approves only non-destructive MCP tools that grant no access`, () => { for (const tool of allowedTools(skill).filter((t) => t.startsWith("mcp__"), )) { - expect(tool).not.toContain("*"); - expect(tool).not.toMatch( - /__(delete|remove|update|protect|unprotect|download|add|assign)_/, - ); + expect(PRE_APPROVABLE_MCP_TOOLS).toContain(tool); } });