diff --git a/README.md b/README.md index 3b91535..538c598 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,66 @@ 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 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. 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. + +Global override example (`~/.pi/agent/agents/claude-code.md`): + +```markdown +--- +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 +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 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 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", + task: "Inspect the auth flow and report where session cookies are created.", +}); +``` + +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 +``` + --- ## 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" }),