@harms-haus/pi-subagents 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,546 @@
1
+ # Profiles Reference
2
+
3
+ Complete reference for the subagent profile system in pi-subagents.
4
+
5
+ > **See also:** [Architecture Overview](architecture.md) · [Tools Reference](tools-reference.md)
6
+
7
+ ---
8
+
9
+ ## 1. Overview
10
+
11
+ Profiles are named, reusable configurations that pre-configure how a sub-agent runs. Instead of specifying provider, model, system prompt, thinking level, tool restrictions, and other settings inline with every task, you define them once in a profile and reference it by name.
12
+
13
+ Profiles are useful for:
14
+
15
+ - **Consistency** — ensure every code-review task uses the same model and instructions
16
+ - **Specialization** — maintain separate profiles for different roles (reviewer, researcher, planner, etc.)
17
+ - **Security** — restrict tool access or extensions for untrusted tasks
18
+ - **Convenience** — swap models or providers globally by editing a single file
19
+
20
+ Each profile is a standalone Markdown file with YAML frontmatter. The frontmatter defines configuration; the Markdown body becomes the sub-agent's system prompt.
21
+
22
+ ---
23
+
24
+ ## 2. Profile File Format
25
+
26
+ Profiles are Markdown files (`.md`) stored in the [agent-profiles directory](#4-profile-locations--resolution). They use YAML frontmatter for configuration and Markdown body for the system prompt.
27
+
28
+ ### Structure
29
+
30
+ ```markdown
31
+ ---
32
+ name: my-profile
33
+ provider: anthropic
34
+ model: claude-sonnet-4-5
35
+ thinkingLevel: high
36
+ tools: read,bash,grep
37
+ noExtensions: true
38
+ ---
39
+
40
+ You are an expert code reviewer. Focus on bugs, security issues, and
41
+ performance. Be thorough but concise in your feedback.
42
+ ```
43
+
44
+ ### Frontmatter Fields
45
+
46
+ Every field in the frontmatter is **optional**. Only `name` is required for the profile to be loaded.
47
+
48
+ | Field | Type | Description |
49
+ | -------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
+ | `name` | `string` | **Required.** The profile's identifier. Must match `[a-zA-Z0-9_-]+`. Determines the filename (`{name}.md`). Note: `name` is not part of the `SubagentProfile` type — it is extracted during loading and used as the key in the profiles map. |
51
+ | `provider` | `string` | LLM provider name (e.g., `anthropic`, `openai`, `dashscope`). Maps to `--provider`. |
52
+ | `model` | `string` | Model ID or pattern. Supports `provider/id` format (e.g., `anthropic/claude-sonnet-4-5`) and `:thinking` shorthand (e.g., `sonnet:high`). Maps to `--model`. |
53
+ | `thinkingLevel` | `string` | Extended thinking level. Valid values: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`. Maps to `--thinking`. |
54
+ | `tools` | `string` or `string[]` | Comma-separated string **or** YAML array of tool names to **allow**. If set, only these tools are available. E.g., `read,bash,grep` or `[read, bash, grep]`. Maps to `--tools`. |
55
+ | `noTools` | `boolean` | If `true`, disables all tools for the sub-agent. Maps to `--no-tools`. Mutually exclusive with `tools` and `excludeTools` — if multiple are set, precedence is `noTools` > `tools` > `excludeTools`. |
56
+ | `excludeTools` | `string` or `string[]` | Comma-separated string **or** YAML array of tool names to **exclude** from the parent session's full tool set. The allowed tools are computed at spawn time: all available tools minus the blacklisted ones, passed as `--tools` to the child process. Mutually exclusive with `tools` — if both are set, an error is thrown. If `noTools` is also set, `noTools` takes precedence and `excludeTools` is silently ignored. |
57
+ | `noExtensions` | `boolean` | If `true`, disables all extensions. Maps to `--no-extensions`. |
58
+ | `extensions` | `string` or `string[]` | Comma-separated string **or** YAML array of extension file paths to load. Each entry maps to a separate `--extension` flag. |
59
+ | `noSkills` | `boolean` | If `true`, disables skills. Maps to `--no-skills`. Mutually exclusive with `suggestedSkills` and `loadSkills` — if combined, an error is thrown. |
60
+ | `suggestedSkills` | `string` or `string[]` | Comma-separated string **or** YAML array of skill names to _suggest_ to the sub-agent via `--skill` CLI flags. The sub-agent's model decides whether to load them. E.g., `web-search,code-review` or `[web-search, code-review]`. Mutually exclusive with `noSkills`. |
61
+ | `loadSkills` | `string` or `string[]` | Comma-separated string **or** YAML array of skill names to _pre-load_ into the sub-agent's system prompt. Skill content is read at spawn time, stripped of frontmatter, and injected into `appendSystemPrompt` wrapped in `<loaded_skill>` XML tags. Mutually exclusive with `noSkills`. |
62
+ | `noContextFiles` | `boolean` | If `true`, disables context files (`AGENTS.md`, `CLAUDE.md`). Maps to `--no-context-files`. |
63
+ | `appendSystemPrompt` | `string` | Text appended to pi's default system prompt. Use when you want to add instructions without replacing the default entirely. Maps to `--append-system-prompt`. |
64
+ | `apiKey` | `string` | Custom API key for this profile. Stored as the `PI_API_KEY` environment variable — **never passed as a CLI argument**. |
65
+ | `extraArgs` | `string` or `string[]` | Comma-separated string **or** YAML array of additional CLI arguments passed verbatim to the sub-agent. Subject to [security validation](#8-security-considerations). |
66
+
67
+ ### Markdown Body → `systemPrompt`
68
+
69
+ The Markdown body (everything after the closing `---`) becomes the sub-agent's `systemPrompt`, mapping to the `--system-prompt` CLI flag. This **replaces** pi's default system prompt entirely.
70
+
71
+ - Leading and trailing whitespace is trimmed.
72
+ - If the body is empty, no `--system-prompt` flag is emitted.
73
+ - Use `appendSystemPrompt` if you want to **add to** the default system prompt instead of replacing it.
74
+
75
+ ---
76
+
77
+ ## 3. Profile Locations & Resolution
78
+
79
+ ### Directory Paths
80
+
81
+ | Scope | Directory | Example |
82
+ | ----------------- | ------------------------------------------------ | ------------------------------------------------ |
83
+ | **Global** | `~/.pi/agent/agent-profiles/` | `~/.pi/agent/agent-profiles/code-reviewer.md` |
84
+ | **Project-local** | `.pi/agent-profiles/` (relative to project root) | `my-project/.pi/agent-profiles/code-reviewer.md` |
85
+
86
+ The global directory respects the `PI_AGENT_DIR` environment variable. If set, the base path is `$PI_AGENT_DIR` instead of `~/.pi/agent`.
87
+
88
+ ### Override Behavior
89
+
90
+ Project-local profiles **override** global profiles with the same `name`. Loading order:
91
+
92
+ 1. Global profiles are loaded first
93
+ 2. Project-local profiles are loaded second, overwriting any global profile with a matching name
94
+
95
+ ### Caching
96
+
97
+ Profiles are cached in memory with a **5-second TTL**. The cache is keyed by the working directory (`cwd`). Cache invalidation happens automatically on:
98
+
99
+ - `saveProfile()` — creating or updating a profile
100
+ - `deleteProfile()` — removing a profile
101
+
102
+ You can also manually clear the cache by calling `invalidateProfilesCache()`.
103
+
104
+ ---
105
+
106
+ ## 4. Complete Examples
107
+
108
+ ### Minimal Profile
109
+
110
+ The simplest possible profile — just sets a model. All other settings use pi defaults.
111
+
112
+ ```markdown
113
+ ---
114
+ name: fast
115
+ model: anthropic/claude-sonnet-4-5
116
+ ---
117
+ ```
118
+
119
+ ### Code Reviewer
120
+
121
+ A thorough reviewer with high thinking, a restricted tool set, and a custom system prompt.
122
+
123
+ ```markdown
124
+ ---
125
+ name: code-reviewer
126
+ provider: anthropic
127
+ model: claude-sonnet-4-5
128
+ thinkingLevel: high
129
+ tools: read,bash,grep,edit,write
130
+ ---
131
+
132
+ You are an expert code reviewer. Analyze code for:
133
+
134
+ 1. **Bugs** — logic errors, race conditions, edge cases
135
+ 2. **Security** — injection, auth issues, data leaks
136
+ 3. **Performance** — unnecessary allocations, inefficient algorithms
137
+ 4. **Maintainability** — clarity, naming, structure
138
+
139
+ Be thorough but concise. Report findings in order of severity.
140
+ ```
141
+
142
+ ### Fast Worker
143
+
144
+ Optimized for speed — no thinking, no extensions, brief responses.
145
+
146
+ ```markdown
147
+ ---
148
+ name: fast-worker
149
+ provider: dashscope
150
+ model: qwen3.5-plus
151
+ thinkingLevel: off
152
+ appendSystemPrompt: Be concise. Skip explanations unless explicitly asked. Return only code or direct answers.
153
+ noExtensions: true
154
+ ---
155
+ ```
156
+
157
+ ### Restricted Sandbox
158
+
159
+ Maximum isolation — no tools, no extensions, no skills, no context files. Ideal for running untrusted or exploratory prompts.
160
+
161
+ ```markdown
162
+ ---
163
+ name: sandbox
164
+ model: openai/gpt-4o
165
+ noTools: true
166
+ noExtensions: true
167
+ noSkills: true
168
+ noContextFiles: true
169
+ ---
170
+
171
+ You are in a restricted environment with no tool access. Answer questions using your training knowledge only. If you need to read files or run commands, explain what you would do but cannot execute it.
172
+ ```
173
+
174
+ ### Read-Only Worker
175
+
176
+ A profile that excludes write-capable tools while retaining read and search access.
177
+
178
+ ```markdown
179
+ ---
180
+ name: read-only-worker
181
+ provider: zai
182
+ model: glm-5.1
183
+ thinkingLevel: medium
184
+ excludeTools: write,edit,bash
185
+ ---
186
+
187
+ You are a read-only analyst. You can read files and search but cannot modify anything.
188
+ ```
189
+
190
+ ### Profile with Suggested Skills
191
+
192
+ A profile that suggests skills to the sub-agent. The model receives `--skill` flags and decides whether to load them based on the task.
193
+
194
+ ```markdown
195
+ ---
196
+ name: skillful-reviewer
197
+ provider: anthropic
198
+ model: claude-sonnet-4-5
199
+ thinkingLevel: high
200
+ suggestedSkills: workflow-generation,code-review
201
+ ---
202
+
203
+ You are a code reviewer with access to specialized skills.
204
+ ```
205
+
206
+ ### Profile with Pre-Loaded Skills
207
+
208
+ A profile that pre-loads skill content directly into the system prompt. The skill instructions are always available — the model doesn't need to discover or choose them.
209
+
210
+ ```markdown
211
+ ---
212
+ name: workflow-builder
213
+ provider: anthropic
214
+ model: claude-sonnet-4-5
215
+ loadSkills: workflow-generation
216
+ appendSystemPrompt: Always validate workflows before saving.
217
+ ---
218
+
219
+ You are a workflow construction specialist.
220
+ ```
221
+
222
+ The `workflow-generation` skill's `SKILL.md` body is read, stripped of its YAML frontmatter, and injected into `appendSystemPrompt` wrapped in `<loaded_skill>` XML. The resulting system prompt append looks like:
223
+
224
+ ```
225
+ Always validate workflows before saving.
226
+
227
+ <loaded_skill name="workflow-generation">
228
+ ... skill content ...
229
+ </loaded_skill>
230
+ ```
231
+
232
+ ### Profile with API Key
233
+
234
+ Uses a dedicated API key, useful for quota management or multi-account setups.
235
+
236
+ ```markdown
237
+ ---
238
+ name: research-budget
239
+ provider: openai
240
+ model: gpt-4o-mini
241
+ apiKey: sk-proj-abc123def456ghi789
242
+ appendSystemPrompt: You are a research assistant. Use web search to find current information. Cite your sources.
243
+ extensions: /path/to/web-search-extension.js
244
+ ---
245
+ ```
246
+
247
+ > **Security note:** The `apiKey` is placed into the `PI_API_KEY` environment variable at runtime, never exposed via `/proc/PID/cmdline`. Still, treat profile files as sensitive — they may contain API keys.
248
+
249
+ ---
250
+
251
+ ## 5. Using Profiles in Tasks
252
+
253
+ Profiles can be applied at two levels when calling `delegate_to_subagents`.
254
+
255
+ ### Per-Task Profile
256
+
257
+ Each task specifies its own profile via the `profile` field:
258
+
259
+ ```json
260
+ {
261
+ "delegate_to_subagents": {
262
+ "tasks": [
263
+ {
264
+ "name": "review-src",
265
+ "prompt": "Review all TypeScript files in src/.",
266
+ "profile": "code-reviewer"
267
+ },
268
+ {
269
+ "name": "research-patterns",
270
+ "prompt": "Research best practices for error handling in TypeScript.",
271
+ "profile": "research-budget"
272
+ }
273
+ ]
274
+ }
275
+ }
276
+ ```
277
+
278
+ ### Top-Level Default Profile
279
+
280
+ Set a default profile for all tasks at the top level. Individual tasks can still override with their own `profile`:
281
+
282
+ ```json
283
+ {
284
+ "delegate_to_subagents": {
285
+ "profile": "fast-worker",
286
+ "tasks": [
287
+ {
288
+ "name": "quick-task",
289
+ "prompt": "Count the lines in src/."
290
+ },
291
+ {
292
+ "name": "deep-review",
293
+ "prompt": "Perform a deep security audit of auth.ts.",
294
+ "profile": "code-reviewer"
295
+ }
296
+ ]
297
+ }
298
+ }
299
+ ```
300
+
301
+ ### Resolution Order
302
+
303
+ | Priority | Source | Example |
304
+ | --------------- | ----------------------------- | ------------------------------- |
305
+ | **1 (highest)** | Per-task `profile` field | `tasks[0].profile` |
306
+ | **2** | Top-level `profile` parameter | `delegate_to_subagents.profile` |
307
+ | **3 (lowest)** | No profile — pi defaults | Neither field set |
308
+
309
+ If a profile name cannot be resolved (doesn't exist in any profiles directory), the task fails with an error:
310
+
311
+ ```
312
+ Unknown profile: "nonexistent". Available profiles: code-reviewer, fast-worker, sandbox
313
+ ```
314
+
315
+ ### Listing Available Profiles
316
+
317
+ The LLM can discover available profiles via the `list_subagent_profiles` tool:
318
+
319
+ ```json
320
+ {
321
+ "list_subagent_profiles": {}
322
+ }
323
+ ```
324
+
325
+ Or interactively with the [`/profile` command](#6-the-profile-command).
326
+
327
+ ---
328
+
329
+ ## 6. The `/profile` Command
330
+
331
+ Manage profiles interactively from the TUI. The `/profile` command provides a full CRUD interface.
332
+
333
+ ### Subcommands
334
+
335
+ | Command | Aliases | Description |
336
+ | ------------------------ | --------------- | ----------------------------------------------------- |
337
+ | `/profile list` | `ls` | List all loaded profiles with one-line summaries |
338
+ | `/profile show <name>` | `<name>` (bare) | Display full details of a profile |
339
+ | `/profile create <name>` | `new` | Launch interactive wizard to create a new profile |
340
+ | `/profile edit <name>` | — | Launch interactive wizard to edit an existing profile |
341
+ | `/profile delete <name>` | `rm`, `remove` | Delete a profile (confirms first) |
342
+
343
+ ### Name Validation
344
+
345
+ Profile names must match the pattern `^[a-zA-Z0-9_-]+$`:
346
+
347
+ - **Allowed:** letters, digits, hyphens (`-`), underscores (`_`)
348
+ - **Not allowed:** spaces, dots, slashes, special characters
349
+ - **Case-sensitive:** `CodeReviewer` and `code-reviewer` are distinct
350
+
351
+ ### Interactive Editor Walkthrough
352
+
353
+ Both `/profile create` and `/profile edit` launch a step-by-step wizard:
354
+
355
+ 1. **Scope** — choose `Global` (`~/.pi/agent/agent-profiles/`) or `Project` (`.pi/agent-profiles/`)
356
+ 2. **Provider** — enter provider name (pre-filled on edit); skip to omit
357
+ 3. **Model** — enter model ID (pre-filled on edit); skip to omit
358
+ 4. **System prompt** — confirm whether to set/keep a custom system prompt; if yes, opens a full editor for the prompt text
359
+ 5. **Append system prompt** — confirm whether to set/keep appended text; if yes, enter the text
360
+ 6. **Thinking level** — confirm whether to set; if yes, select from: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`
361
+ 7. **Tools** — confirm whether to configure; if yes, choose from three options:
362
+ - **Disable all tools** (`noTools`) — sub-agent runs with no tool access
363
+ - **Allowlist** — enter a comma-separated list of tools to _allow_ (all others excluded)
364
+ - **Blacklist** — enter a comma-separated list of tools to _exclude_ (all others allowed)
365
+ 8. **Extensions** — confirm whether to configure; if yes, choose to disable all extensions (`noExtensions`) or enter comma-separated paths
366
+ 9. **Skills** — if skill configuration already exists (`suggestedSkills` or `loadSkills`), offers to remove it. Otherwise, asks whether to configure skills. If yes, prompts for two fields:
367
+ - **Suggested skills** — comma-separated skill names (the model chooses whether to load them)
368
+ - **Pre-loaded skills** — comma-separated skill names (content injected into the system prompt)
369
+ 10. **Review** — displays the full profile summary; confirm to save or cancel
370
+
371
+ At any step, answering "No" or canceling skips that field. Skipped fields are omitted from the saved profile (pi defaults apply).
372
+
373
+ ---
374
+
375
+ ## 7. Profile → CLI Argument Mapping
376
+
377
+ Profiles are converted to CLI arguments and environment variables via `profileToArgs()`. The resulting args are injected into the `pi` subprocess invocation for the sub-agent.
378
+
379
+ | Profile Field | CLI Flag | Notes |
380
+ | -------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
381
+ | `provider` | `--provider <value>` | |
382
+ | `model` | `--model <value>` | |
383
+ | `systemPrompt` | `--system-prompt <value>` | Sourced from Markdown body |
384
+ | `appendSystemPrompt` | `--append-system-prompt <value>` | |
385
+ | `thinkingLevel` | `--thinking <value>` | |
386
+ | `tools` | `--tools <comma-separated>` | Joined with commas: `read,bash,grep` |
387
+ | `noTools` | `--no-tools` | Boolean flag; no value |
388
+ | `excludeTools` | `--tools <computed>` | Resolved at runtime via `applyExcludeTools()`: all parent tools minus blacklisted names, joined into a comma-separated `--tools` value |
389
+ | `noExtensions` | `--no-extensions` | Boolean flag; no value |
390
+ | `extensions` | `--extension <value>` (×N) | One `--extension` flag per entry |
391
+ | `noSkills` | `--no-skills` | Boolean flag; no value |
392
+ | `suggestedSkills` | `--skill <path>` (×N) | After [resolution](#skill-resolution-pipeline), each skill name becomes a file path; one `--skill` flag per entry |
393
+ | `loadSkills` | Merged into `--append-system-prompt` | Skill content is wrapped in `<loaded_skill name="...">` XML tags and appended to any existing `appendSystemPrompt` text |
394
+ | `noContextFiles` | `--no-context-files` | Boolean flag; no value |
395
+ | `apiKey` | _(env var)_ | Set as `PI_API_KEY` in process environment — **not a CLI flag** |
396
+ | `extraArgs` | _(appended verbatim)_ | Each array element appended directly to the args array |
397
+
398
+ ### `noTools` vs `tools` vs `excludeTools` Precedence
399
+
400
+ Tool configuration follows three-way precedence: **`noTools` > `tools` > `excludeTools`**.
401
+
402
+ - If `noTools` is `true`, only `--no-tools` is emitted — both `tools` and `excludeTools` are ignored.
403
+ - If `noTools` is not set but `tools` is, only `--tools` is emitted with the allowlist — `excludeTools` is ignored.
404
+ - If neither `noTools` nor `tools` is set but `excludeTools` is, the blacklisted tools are subtracted from the parent session's full tool set via `applyExcludeTools()`, and the resulting allowed set is passed as `--tools`.
405
+
406
+ This matches the behavior in `profileToArgs()`:
407
+
408
+ ```typescript
409
+ if (profile.noTools) {
410
+ args.push("--no-tools");
411
+ } else if (profile.tools && profile.tools.length > 0) {
412
+ args.push("--tools", profile.tools.join(","));
413
+ }
414
+ // else: excludeTools is resolved at runtime via applyExcludeTools()
415
+ // to produce a computed tools array, which is then passed as --tools
416
+ ```
417
+
418
+ > **Note:** `applyExcludeTools()` resolves `excludeTools` to a computed `tools` array **before** `profileToArgs()` runs. By the time the extraArgs validation guard executes, the profile already has `tools` set (not `excludeTools`), so the guard checks against `profile.tools` regardless of whether the original profile used `tools` or `excludeTools`.
419
+
420
+ If both `tools` and `excludeTools` are set without `noTools`, an error is thrown — they are mutually exclusive.
421
+
422
+ ### Skill Resolution Pipeline
423
+
424
+ When a profile contains `suggestedSkills` or `loadSkills`, the delegation pipeline resolves skill names to concrete data before spawning the sub-agent. The full pipeline:
425
+
426
+ 1. **Validation** — `validateProfileSkills()` checks that neither `suggestedSkills` nor `loadSkills` is combined with `noSkills`. If they are, an error is thrown before any file I/O occurs.
427
+
428
+ 2. **Discovery** — If any profile needs skill resolution, `discoverSkills()` is called **once** (cached across all tasks in the same delegation call). It scans the agent directory (`~/.pi/agent` or `$PI_AGENT_DIR`) for skill definitions and returns a map of `{ name, filePath, description }`.
429
+
430
+ 3. **Resolution** — `resolveProfileSkills()` processes each field:
431
+ - **`suggestedSkills`** — each name is looked up in the skill map. If found, the name is replaced with the skill's file path. If not found, an error lists the unknown skills and available ones. After resolution, `profile.suggestedSkills` contains file paths (not names).
432
+ - **`loadSkills`** — each name is looked up in the skill map. If found, the skill's `SKILL.md` file is read, frontmatter is stripped via `stripFrontmatter()`, and the body is wrapped in `<loaded_skill name="...">\n...\n</loaded_skill>` XML. All parts are appended to the profile's existing `appendSystemPrompt`. After resolution, `profile.loadSkills` is set to `undefined` (consumed).
433
+
434
+ 4. **CLI emission** — `profileToArgs()` converts resolved `suggestedSkills` paths into individual `--skill <path>` flags. Merged `loadSkills` content is emitted as part of `--append-system-prompt`.
435
+
436
+ This means skill resolution happens **at delegation time**, not at profile-load time. A profile file only stores skill names; the actual file paths and content are resolved when the sub-agent is spawned.
437
+
438
+ ### API Key Handling
439
+
440
+ The `apiKey` field is **never** passed as a CLI argument. Instead, it is set as the `PI_API_KEY` environment variable in the sub-agent's process environment. This prevents the key from appearing in `/proc/PID/cmdline` or process listings.
441
+
442
+ ### Example Conversion
443
+
444
+ Given this profile:
445
+
446
+ ```markdown
447
+ ---
448
+ name: reviewer
449
+ provider: anthropic
450
+ model: claude-sonnet-4-5
451
+ thinkingLevel: high
452
+ tools: read,bash,grep
453
+ noExtensions: true
454
+ apiKey: sk-test-abc123
455
+ extraArgs: --verbose
456
+ ---
457
+
458
+ You are a code reviewer.
459
+ ```
460
+
461
+ The resulting invocation parameters:
462
+
463
+ ```
464
+ args: [
465
+ "--provider", "anthropic",
466
+ "--model", "claude-sonnet-4-5",
467
+ "--system-prompt", "You are a code reviewer.",
468
+ "--thinking", "high",
469
+ "--tools", "read,bash,grep",
470
+ "--no-extensions",
471
+ "--verbose"
472
+ ]
473
+ env: {
474
+ PI_API_KEY: "sk-test-abc123"
475
+ }
476
+ ```
477
+
478
+ ---
479
+
480
+ ## 8. Security Considerations
481
+
482
+ ### API Keys
483
+
484
+ - API keys in profile files are stored as plain text. Treat profile directories as sensitive.
485
+ - At runtime, API keys are passed via the `PI_API_KEY` environment variable, not CLI arguments, avoiding exposure in process listings.
486
+ - The `/profile show` command masks API keys in display output (e.g., `sk-t****bc123`).
487
+
488
+ ### `extraArgs` Validation
489
+
490
+ The `extraArgs` field is subject to security validation before being passed to the subprocess. The following are **blocked**:
491
+
492
+ | Pattern | Example | Error |
493
+ | ------------------------ | ----------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
494
+ | Null bytes | `"test\0arg"` | `Invalid extraArg: contains null byte` |
495
+ | Shell operators at start | `" | ls"`, `"& rm"`, `"; echo"` | `Refusing extraArg: potentially unsafe argument '\| ls...'` |
496
+ | Command separators | `"&& rm"`, `" | | exit"`, `"; ls"` | `Refusing extraArg: potentially unsafe argument '&& rm...'` |
497
+ | Shell redirection | `"> file"`, `">> file"`, `"< file"` | `Refusing extraArg: potentially unsafe argument '> file...'` |
498
+ | Backticks / leading `$` | ``"`whoami`"``, `"$HOME"` | ``Refusing extraArg: potentially unsafe argument '`whoami`...'`` |
499
+
500
+ > **Note:** The validation regex catches backticks and `$` only at the **start** of an argument. It does **not** detect `$()` command substitution when it appears later in the string (e.g., `"arg$(whoami)"` passes validation).
501
+
502
+ The validation regex:
503
+
504
+ ```
505
+ /^[\s|&;$\\`!]|&&|\|\||;|>|>>|<|<</
506
+ ```
507
+
508
+ ### Tool Restriction Override Guard
509
+
510
+ When `excludeTools`, `tools`, or `noTools` is set in a profile, passing tool-restriction flags via `extraArgs` is **blocked**. The following `extraArgs` patterns are rejected:
511
+
512
+ | Condition | Tool | Error Message | Description |
513
+ | ------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
514
+ | Tool restriction override guard | `delegate_to_subagents` | `Refusing extraArg "${arg}" which would override profile tool restrictions. Use the dedicated profile fields instead.` | Thrown when `extraArgs` contains `--tools`, `-t`, `--no-tools`, or `-nt` (including their `=` forms) while the profile has tool restrictions active. |
515
+
516
+ Both space-separated (`--tools read`) and equals-sign (`--tools=read`) forms are caught. This prevents `extraArgs` from silently overriding the profile's tool restrictions.
517
+
518
+ ### Skill Content Injection
519
+
520
+ The `loadSkills` field causes skill file content to be read and injected into the sub-agent's system prompt at spawn time:
521
+
522
+ - Skill content is read directly from disk via `readFileSync` — ensure skill files are from trusted sources.
523
+ - The injected content is wrapped in `<loaded_skill name="...">` XML tags and appended to `appendSystemPrompt`, meaning it is visible to the model as part of the system prompt.
524
+ - Because `loadSkills` content merges into `appendSystemPrompt`, any existing `appendSystemPrompt` text is preserved and the skill content follows it.
525
+ - `loadSkills` and `suggestedSkills` are both mutually exclusive with `noSkills`. If `noSkills` is also set, validation fails with an error — this prevents silent contradictions where skill content would be prepared but `--no-skills` would disable skill discovery.
526
+
527
+ ### Profile File Permissions
528
+
529
+ Consider restricting read access to profile directories if they contain API keys:
530
+
531
+ ```bash
532
+ chmod 700 ~/.pi/agent/agent-profiles/
533
+ chmod 600 ~/.pi/agent/agent-profiles/*.md
534
+ ```
535
+
536
+ ### Project-Local Profiles
537
+
538
+ Be cautious when using project-local profiles in shared repositories — they are committed with the project and visible to all collaborators. Prefer global profiles for sensitive configurations (API keys, etc.).
539
+
540
+ ---
541
+
542
+ ## Related Documentation
543
+
544
+ - [Tools Reference](tools-reference.md) — the `delegate_to_subagents`, `get_subagent_output`, `get_subagent_session`, and `list_subagent_profiles` tools
545
+ - [Architecture Overview](architecture.md) — how sub-agents are spawned and managed
546
+ - [README](../README.md) — installation and quick start
@@ -0,0 +1,52 @@
1
+ # Settings
2
+
3
+ pi-subagents reads configuration from JSON settings files. Unlike profile files, settings control runtime behavior such as display dimensions and truncation limits.
4
+
5
+ ## Settings File Locations
6
+
7
+ Settings are loaded from two locations. Project-local settings override global settings for the same key.
8
+
9
+ | Location | Path |
10
+ | -------- | ---------------------------------- |
11
+ | Global | `~/.pi/agent/settings.json` |
12
+ | Project | `<project-root>/.pi/settings.json` |
13
+
14
+ The global location can be overridden by setting the `PI_AGENT_DIR` environment variable.
15
+
16
+ Both files are optional. If neither exists, defaults are used.
17
+
18
+ ## Settings Reference
19
+
20
+ All pi-subagents settings live under the `"subagents"` key in the settings JSON.
21
+
22
+ | Setting | Type | Default | Description |
23
+ | ----------------------------------- | -------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | `subagents.maxLinesPerWindow` | `number` | `15` | Number of lines displayed in each sub-agent's rolling TUI window. Also controls how much output is shown by `get_subagent_output` and `get_subagent_session`. |
25
+ | `subagents.commandPreviewWidth` | `number` | Terminal width − 4 (TTY) or `160` (non-TTY) | Maximum character width for tool call preview rendering. Clamped to a minimum of `20`. |
26
+ | `subagents.extend_timeout_debounce` | `number` | `30` | When a sub-agent is actively working (making tool calls) when its timeout expires, the timeout is extended by this many seconds. Each new tool call resets the extension timer. The original timeout value is still displayed in the TUI. Clamped to 0–300. Set to `0` to disable timeout extension. |
27
+ | `subagents.looping_tool_count` | `number` | `5` | Number of consecutive identical tool calls required to trigger loop detection. When this many consecutive tool call signatures (serialized as JSON) are identical strings, the sub-agent is immediately killed with an error. Set to `0` to disable loop detection. Clamped to 0–50. |
28
+
29
+ ### `commandPreviewWidth` Resolution Logic
30
+
31
+ This setting has a two-tier resolution:
32
+
33
+ 1. **TTY detected** (`process.stdout.columns` is available): Uses the terminal's column count minus 4 (accounting for a 2-character indent and the `→ ` prefix), clamped to a minimum of 20. Settings files are **not** consulted in this case.
34
+ 2. **Non-TTY** (piped output, CI, etc.): Falls back to settings files (project overrides global), defaulting to `160` if no setting is found, clamped to a minimum of 20.
35
+
36
+ ## Example
37
+
38
+ ```json
39
+ {
40
+ "subagents": {
41
+ "maxLinesPerWindow": 25,
42
+ "commandPreviewWidth": 120,
43
+ "extend_timeout_debounce": 30,
44
+ "looping_tool_count": 5
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Related Configuration
50
+
51
+ - **Profiles** — Named agent profiles (model, tools, system prompt) are defined in separate markdown files. See [docs/profiles.md](profiles.md).
52
+ - **Architecture** — For how settings and profiles fit into the broader system, see [docs/architecture.md](architecture.md).