From 1352c35651426f20d06dbcb67abb1ff38f18752f Mon Sep 17 00:00:00 2001 From: Ben Tang Date: Thu, 7 May 2026 09:20:36 +0800 Subject: [PATCH 1/2] feat(subagents): support Claude Code permission overrides Allow custom Claude Code-backed agents to set permission mode, allowed tools, and disallowed tools from frontmatter while preserving the existing bypass-permissions default when no override is configured. Document the read-only Claude Code agent pattern and add tests for parsing and launch argument generation. --- README.md | 72 ++++++++++++++++++++++++++++++++- pi-extension/subagents/index.ts | 39 +++++++++++++++++- test/test.ts | 45 +++++++++++++++++++++ 3 files changed, 154 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3b91535..eeb233a 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ You are a specialized agent that does X... | `description` | string | Shown in `subagents_list` output | | `model` | string | Default model (e.g. `anthropic/claude-sonnet-4-6`) | | `thinking` | string | Thinking level: `minimal`, `medium`, `high` | -| `tools` | string | Comma-separated **native pi tools only**: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls` | +| `tools` | string | Comma-separated tool allowlist. For Pi-backed agents, use native pi tool names like `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`. For Claude Code-backed agents (`cli: claude`), this is passed to Claude Code as `--tools`, so use Claude Code tool names like `Read`, `Grep`, `Glob`, `Bash`, `Edit`. | | `skills` | string | Comma-separated skill names to auto-load | | `session-mode` | string | Default child-session mode: `standalone`, `lineage-only`, or `fork` | | `spawning` | boolean | Set `false` to deny all subagent-spawning tools | @@ -305,6 +305,13 @@ You are a specialized agent that does X... | `auto-exit` | boolean | Auto-shutdown when the agent finishes its turn — no `subagent_done` call needed. If the user sends any input, auto-exit is permanently disabled and the user takes over the session. Recommended for autonomous agents (scout, worker); not for interactive ones (planner). Also determines the default value of `interactive` (see below). | | `interactive` | boolean | derived | Override whether stall/recovery transitions wake the parent session. Defaults to the inverse of `auto-exit`: autonomous agents (`auto-exit: true`) are non-interactive and get stall pings; agents without `auto-exit` are interactive and stay quiet. Explicit values take precedence. | | `cwd` | string | Default working directory (absolute or relative to project root) | +| `cli` | string | Optional backend CLI. Set `claude` to launch Claude Code instead of Pi for this agent. | +| `permission-mode` | string | Claude Code only. Passed as `claude --permission-mode `. If unset for `cli: claude`, the launcher keeps the legacy `--dangerously-skip-permissions` behavior. | +| `claude-permission-mode` | string | Claude Code only. Explicit alias for `permission-mode`. | +| `allowed-tools` | string | Claude Code only. Alias for `tools`, passed as `claude --tools `. | +| `claude-tools` | string | Claude Code only. Explicit alias for `tools`, passed as `claude --tools `. | +| `disallowed-tools` | string | Claude Code only. Passed as `claude --disallowed-tools `. | +| `claude-disallowed-tools` | string | Claude Code only. Explicit alias for `disallowed-tools`. | | `disable-model-invocation` | boolean | Hide this agent from discovery surfaces like `subagents_list`. The agent still remains directly invokable by explicit name via `subagent({ agent: "name", ... })`. | --- @@ -378,6 +385,69 @@ Or per spawn: subagent({ name: "Scout", agent: "scout", interactive: true, task: "..." }); ``` +### Claude Code agents and read-only mode + +Agents with `cli: claude` launch Claude Code instead of Pi. This is useful when you want a subagent to use a local Claude Code installation while still being spawned and monitored by this extension. + +If a Claude Code agent does not set `permission-mode`, the launcher preserves the bundled `claude-code` behavior and starts Claude Code with `--dangerously-skip-permissions`. To make a safer custom agent, define a separate agent file in `.pi/agents/` or `~/.pi/agent/agents/` and set Claude Code's permission and tool flags there. + +Recommended guidelines: + +- Do not edit the bundled `agents/claude-code.md` just to change permissions. Create a separate project-local or global agent instead. +- Use Claude Code tool names, not Pi tool names, for Claude Code agents. Examples: `Read`, `Grep`, `Glob`, `Bash`, `Edit`, `Write`. +- For read-only investigation, allow only read-oriented Claude Code tools and deny mutation or shell tools. +- Use `spawning: false` and `deny-tools: claude` when the child should not delegate or call back into Claude Code from Pi tools. +- Keep `auto-exit: true` for autonomous read-only investigations so the pane closes and reports back when done. + +Project-local read-only example (`.pi/agents/claude-code-readonly.md`): + +```markdown +--- +name: claude-code-readonly +description: Read-only Claude Code investigation session +cli: claude +model: sonnet +auto-exit: true +spawning: false +deny-tools: claude +permission-mode: default +tools: Read,Grep,Glob +disallowed-tools: Bash,Edit,Write +--- + +# Claude Code Read-only + +You are a read-only Claude Code investigator. Inspect files and report findings with evidence. Do not edit files, run shell commands, or make changes. +``` + +Then spawn it like any other named agent: + +```typescript +subagent({ + name: "Read-only investigation", + agent: "claude-code-readonly", + task: "Inspect the auth flow and report where session cookies are created.", +}); +``` + +You can also use explicit Claude-prefixed field names if you want the frontmatter to make the backend distinction obvious: + +```yaml +--- +name: claude-code-readonly +cli: claude +claude-permission-mode: default +claude-tools: Read,Grep,Glob +claude-disallowed-tools: Bash,Edit,Write +--- +``` + +The extension passes these through to Claude Code as: + +```bash +claude --permission-mode default --tools Read,Grep,Glob --disallowed-tools Bash,Edit,Write +``` + --- ## Tool Access Control diff --git a/pi-extension/subagents/index.ts b/pi-extension/subagents/index.ts index 8f2dba9..fc48ef5 100644 --- a/pi-extension/subagents/index.ts +++ b/pi-extension/subagents/index.ts @@ -137,6 +137,9 @@ interface AgentDefaults { skills?: string; thinking?: string; denyTools?: string; + claudePermissionMode?: string; + claudeAllowedTools?: string; + claudeDisallowedTools?: string; spawning?: boolean; autoExit?: boolean; interactive?: boolean; @@ -242,6 +245,16 @@ function parseAgentDefinition(content: string, fallbackName: string): AgentDefin skills: getFrontmatterValue(frontmatter, "skill") ?? getFrontmatterValue(frontmatter, "skills"), thinking: getFrontmatterValue(frontmatter, "thinking"), denyTools: getFrontmatterValue(frontmatter, "deny-tools"), + claudePermissionMode: + getFrontmatterValue(frontmatter, "claude-permission-mode") ?? + getFrontmatterValue(frontmatter, "permission-mode"), + claudeAllowedTools: + getFrontmatterValue(frontmatter, "claude-tools") ?? + getFrontmatterValue(frontmatter, "allowed-tools") ?? + getFrontmatterValue(frontmatter, "tools"), + claudeDisallowedTools: + getFrontmatterValue(frontmatter, "claude-disallowed-tools") ?? + getFrontmatterValue(frontmatter, "disallowed-tools"), spawning: parseOptionalBoolean(getFrontmatterValue(frontmatter, "spawning")), autoExit: parseOptionalBoolean(getFrontmatterValue(frontmatter, "auto-exit")), interactive: parseOptionalBoolean(getFrontmatterValue(frontmatter, "interactive")), @@ -686,6 +699,29 @@ function buildSubagentToolAllowlist(effectiveTools?: string): string | null { return [...allow].join(","); } +function buildClaudePermissionArgs(agentDefs: AgentDefaults | null): string[] { + const args: string[] = []; + const permissionMode = agentDefs?.claudePermissionMode?.trim(); + + if (permissionMode) { + args.push("--permission-mode", shellEscape(permissionMode)); + } else { + args.push("--dangerously-skip-permissions"); + } + + const allowedTools = agentDefs?.claudeAllowedTools?.trim(); + if (allowedTools) { + args.push("--tools", shellEscape(allowedTools)); + } + + const disallowedTools = agentDefs?.claudeDisallowedTools?.trim(); + if (disallowedTools) { + args.push("--disallowed-tools", shellEscape(disallowedTools)); + } + + return args; +} + function buildPiPromptArgs(params: { effectiveSkills?: string; taskDelivery: "direct" | "artifact"; @@ -902,6 +938,7 @@ export const __test__ = { resolveLaunchBehavior, resolveEffectiveInteractive, buildSubagentToolAllowlist, + buildClaudePermissionArgs, buildPiPromptArgs, formatWidgetRightLabel, observeRunningSubagent, @@ -1014,7 +1051,7 @@ async function launchSubagent( const cmdParts: string[] = []; cmdParts.push(`PI_CLAUDE_SENTINEL=${shellEscape(sentinelFile)}`); cmdParts.push("claude"); - cmdParts.push("--dangerously-skip-permissions"); + cmdParts.push(...buildClaudePermissionArgs(agentDefs)); if (existsSync(pluginDir)) { cmdParts.push("--plugin-dir", shellEscape(pluginDir)); diff --git a/test/test.ts b/test/test.ts index 5503bb8..340b3a3 100644 --- a/test/test.ts +++ b/test/test.ts @@ -884,6 +884,28 @@ describe("subagent discovery", () => { }); }); + it("loads Claude Code permission and tool overrides from frontmatter", async () => { + await withIsolatedAgentEnv(async ({ projectAgentsDir }) => { + writeAgentFile( + projectAgentsDir, + "claude-readonly-test-agent", + [ + "name: claude-readonly-test-agent", + "cli: claude", + "permission-mode: default", + "tools: Read,Grep,Glob", + "disallowed-tools: Bash,Edit,Write", + ].join("\n"), + ); + + const loaded = testApi.loadAgentDefaults("claude-readonly-test-agent"); + assert.ok(loaded, "expected agent to load"); + assert.equal(loaded.claudePermissionMode, "default"); + assert.equal(loaded.claudeAllowedTools, "Read,Grep,Glob"); + assert.equal(loaded.claudeDisallowedTools, "Bash,Edit,Write"); + }); + }); + it("loads explicit interactive flag from frontmatter", async () => { await withIsolatedAgentEnv(async ({ projectAgentsDir }) => { writeAgentFile( @@ -1091,6 +1113,29 @@ describe("subagent discovery", () => { assert.equal(testApi.buildSubagentToolAllowlist(""), null); }); + it("buildClaudePermissionArgs keeps legacy bypass permissions by default", () => { + assert.deepEqual(testApi.buildClaudePermissionArgs(null), ["--dangerously-skip-permissions"]); + assert.deepEqual(testApi.buildClaudePermissionArgs({}), ["--dangerously-skip-permissions"]); + }); + + it("buildClaudePermissionArgs supports read-only Claude Code overrides", () => { + assert.deepEqual( + testApi.buildClaudePermissionArgs({ + claudePermissionMode: "default", + claudeAllowedTools: "Read,Grep,Glob", + claudeDisallowedTools: "Bash,Edit,Write", + }), + [ + "--permission-mode", + "'default'", + "--tools", + "'Read,Grep,Glob'", + "--disallowed-tools", + "'Bash,Edit,Write'", + ], + ); + }); + it("buildPiPromptArgs inserts separator for artifact-backed launches with skills", () => { assert.deepEqual( testApi.buildPiPromptArgs({ effectiveSkills: "review,lint", taskDelivery: "artifact", taskArg: "@artifact.md" }), From 83d9a0dd3a988398cb9a491642e07723b5a55049 Mon Sep 17 00:00:00 2001 From: Ben Tang Date: Thu, 7 May 2026 09:42:51 +0800 Subject: [PATCH 2/2] docs: show same-name Claude Code read-only override Update the read-only Claude Code documentation to demonstrate overriding the bundled claude-code agent with a same-name global or project-local agent file, matching the supported agent discovery precedence. --- README.md | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index eeb233a..538c598 100644 --- a/README.md +++ b/README.md @@ -389,60 +389,57 @@ subagent({ name: "Scout", agent: "scout", interactive: true, task: "..." }); Agents with `cli: claude` launch Claude Code instead of Pi. This is useful when you want a subagent to use a local Claude Code installation while still being spawned and monitored by this extension. -If a Claude Code agent does not set `permission-mode`, the launcher preserves the bundled `claude-code` behavior and starts Claude Code with `--dangerously-skip-permissions`. To make a safer custom agent, define a separate agent file in `.pi/agents/` or `~/.pi/agent/agents/` and set Claude Code's permission and tool flags there. +If a Claude Code agent does not set `permission-mode`, the launcher preserves the bundled `claude-code` behavior and starts Claude Code with `--dangerously-skip-permissions`. To make the default `claude-code` agent safer, override that bundled agent by creating a same-name file in `.pi/agents/claude-code.md` or `~/.pi/agent/agents/claude-code.md` and set Claude Code's permission and tool flags there. Recommended guidelines: -- Do not edit the bundled `agents/claude-code.md` just to change permissions. Create a separate project-local or global agent instead. +- Do not edit the bundled `agents/claude-code.md` just to change permissions. Override it with a same-name project-local or global agent file. - Use Claude Code tool names, not Pi tool names, for Claude Code agents. Examples: `Read`, `Grep`, `Glob`, `Bash`, `Edit`, `Write`. - For read-only investigation, allow only read-oriented Claude Code tools and deny mutation or shell tools. - Use `spawning: false` and `deny-tools: claude` when the child should not delegate or call back into Claude Code from Pi tools. - Keep `auto-exit: true` for autonomous read-only investigations so the pane closes and reports back when done. -Project-local read-only example (`.pi/agents/claude-code-readonly.md`): +Global override example (`~/.pi/agent/agents/claude-code.md`): ```markdown --- -name: claude-code-readonly -description: Read-only Claude Code investigation session +name: claude-code +description: Read-only Claude Code session for investigation and code exploration cli: claude model: sonnet auto-exit: true spawning: false deny-tools: claude -permission-mode: default -tools: Read,Grep,Glob -disallowed-tools: Bash,Edit,Write +claude-permission-mode: default +claude-tools: Read,Grep,Glob +claude-disallowed-tools: Bash,Edit,Write --- # Claude Code Read-only -You are a read-only Claude Code investigator. Inspect files and report findings with evidence. Do not edit files, run shell commands, or make changes. +You are a read-only Claude Code session spawned by pi for investigation and code exploration. + +You may inspect files with Read, Grep, and Glob. Do not edit files, run shell commands, change repository state, install packages, run tests, or make network calls. + +## Guidelines + +- Focus on the task given to you. +- Report concrete findings with evidence, including file paths and relevant excerpts. +- If you need information that requires shell commands, edits, builds, tests, or network access, explain what is needed instead of attempting it. +- Your final message should summarize what you found and what you could not verify under read-only constraints. ``` -Then spawn it like any other named agent: +Then keep using the normal bundled agent name. Agent discovery gives your global or project-local file precedence over the package-bundled definition: ```typescript subagent({ name: "Read-only investigation", - agent: "claude-code-readonly", + agent: "claude-code", task: "Inspect the auth flow and report where session cookies are created.", }); ``` -You can also use explicit Claude-prefixed field names if you want the frontmatter to make the backend distinction obvious: - -```yaml ---- -name: claude-code-readonly -cli: claude -claude-permission-mode: default -claude-tools: Read,Grep,Glob -claude-disallowed-tools: Bash,Edit,Write ---- -``` - -The extension passes these through to Claude Code as: +The extension passes the Claude-prefixed fields through to Claude Code as: ```bash claude --permission-mode default --tools Read,Grep,Glob --disallowed-tools Bash,Edit,Write