Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,14 +297,21 @@ 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 |
| `deny-tools` | string | Comma-separated extension tool names to deny |
| `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 <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 <tools>`. |
| `claude-tools` | string | Claude Code only. Explicit alias for `tools`, passed as `claude --tools <tools>`. |
| `disallowed-tools` | string | Claude Code only. Passed as `claude --disallowed-tools <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", ... })`. |

---
Expand Down Expand Up @@ -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
Expand Down
39 changes: 38 additions & 1 deletion pi-extension/subagents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ interface AgentDefaults {
skills?: string;
thinking?: string;
denyTools?: string;
claudePermissionMode?: string;
claudeAllowedTools?: string;
claudeDisallowedTools?: string;
spawning?: boolean;
autoExit?: boolean;
interactive?: boolean;
Expand Down Expand Up @@ -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")),
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -902,6 +938,7 @@ export const __test__ = {
resolveLaunchBehavior,
resolveEffectiveInteractive,
buildSubagentToolAllowlist,
buildClaudePermissionArgs,
buildPiPromptArgs,
formatWidgetRightLabel,
observeRunningSubagent,
Expand Down Expand Up @@ -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));
Expand Down
45 changes: 45 additions & 0 deletions test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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" }),
Expand Down