# Mode file format

A mode is one markdown file: YAML frontmatter + a markdown body.

```
<modes dir>/<name>.md
```

- **Frontmatter** — structured config, validated with typebox at load.
- **Body** — the mode's instructions, injected verbatim into the session
  (as a hidden message) on every user prompt while the mode is active.

## Frontmatter fields

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `name` | string | **yes** | Mode name. Must equal the filename stem (`plan.md` → `plan`). kebab-case. |
| `tools` | object | no | Toolset/guardrail config (below). |
| `tools.allow` | string[] | no | Restrict the active toolset to exactly these tools. |
| `tools.deny` | string[] | no | Remove these tools from the active set. |
| `tools.shell` | boolean | no | `false` hides and blocks recognized built-in shell tools (`bash`, `powershell`) independent of the host platform; `true`/omitted leaves them subject to `allow`/`deny`. |
| `tools.bashAllow` | string[] | no | Optional best-effort command-text filter for Bash/PowerShell; not a shell parser or security boundary. |
| `tools.bashDeny` | string[] | no | Optional best-effort substring filter for Bash/PowerShell; not a security boundary. |
| `model` | string | no | **Experimental.** `provider/model-id`, applied on entry via `pi.setModel`. |
| `thinking` | string | no | **Experimental.** Thinking level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`), applied on entry. |

### `tools` semantics

- `allow` restricts the active set and `deny` removes tools; when both are
  present, `allow` is applied first and `deny` filters its result.
- The active set is always derived from the **session baseline** (the
  toolset captured before the first activation), never from the current
  active set — so mode → mode switches never compound.
- Policy names match Pi tool-call names exactly. The Pi 0.85.x built-ins
  include `read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, and
  `ls`; other extensions may add tools. Unknown names do not create a tool,
  and policies can match third-party tool names if they use the same name.
- `shell: false` removes registered `bash` and `powershell` tools from the
  active set and independently blocks calls through Pi's tool-call hook.
  These are the recognized built-in shell names in the supported Pi runtime.
- `bashAllow` / `bashDeny` run only when the shell is not disabled. They
  inspect command text heuristically. They are not a complete parser and do
  not guarantee safety; use `shell: false` when the workflow requires no shell.

### Best-effort shell filters

When a filter is configured, the current implementation uses these heuristic checks:

1. **Destructive-pattern pass** — mutations (`rm`, `mv`, `cp`, `mkdir`, `tee`,
   `dd`, `sed -i`, …), shell redirection (`>`, `>>`), privilege/process
   control (`sudo`, `kill`, …), terminal editors (`vim`/`nano`/`emacs`),
   package installs (`npm install`, `apt install`, …), mutating git
   (`git push`, `git reset`, …), and PowerShell mutators
   (`Remove-Item`, `Set-Content`, `Out-File`, …) are blocked even when they
   begin with an allowlisted command.
2. **Command/process substitution** — `$(…)`, `<(...)`, `>(...)`, and
   backticks run arbitrary code and are blocked outright.
3. **Command-position matching** — the command is split on `;`, `&&`, `||`,
   `|`, `&`, and newlines, and *every* simple-command segment must **begin
   with** an allowlisted command (the leading token(s), followed by a word
   boundary). `git log | head` keeps working; `git log | python3 -c …` and
   `ls | xargs python3 -c …` cannot smuggle an interpreter, because
   `python3`/`xargs` is not the *command* of its segment. Matching the
   command (not the arguments) means `which node` is still allowed (`node`
   is an argument, not a command).

The checks are regression-tested examples, not evidence that arbitrary shell
commands are safe. `bashDeny`-only modes apply substring checks to raw text.

### Shell coverage

The supported Pi API exposes `bash` and `powershell` built-in tools. The
semantic `shell: false` policy blocks both names even if one is not registered
or is still present in a caller's active tool list. Calls to tools provided by
other extensions are guarded only when they pass through Pi's `tool_call`
hook; effects outside that path are out of scope.

### `model` / `thinking` (experimental)

Applied when the mode is entered, restored to the session's pre-activation
values when the mode is cleared. Both are session-scoped and recorded in
session history, so pi restores them on resume. Omitted → untouched.
Unknown models/levels are ignored with a warning.

## Body rules

- Injected **verbatim** — no interpolation, no timestamps (byte-stable across
  turns so provider prefix caching works).
- Keep it a short contract (~100–300 tokens). Never a giant "personality"
  block.
- Workflows MUST declare an output format (report shape, plan shape, …).
  Do not call a workflow unconditionally read-only: tool filters do not
  constrain effects outside Pi's tool-call path.

## Validation

- Frontmatter is parsed with `yaml` and validated against the typebox schema
  (`ModeFrontmatterSchema` in `src/schema.ts`).
- `name` mismatch vs filename stem → **error**, file skipped.
- Non-kebab-case names (`my mode`) → **error** — they cannot be addressed
  via `/mode` (args are split on whitespace).
- Reserved names colliding with `/mode` subcommands (`new`, `reload`,
  `default`) → **error**, file skipped.
- Unknown frontmatter keys → **warning**, file still loaded (forward-compat).
- Invalid YAML / missing frontmatter / non-mapping → **error**, file skipped
  with a warning notification. Discovery never crashes the session.
- Empty body → **warning** (mode injects no instructions).

## Discovery & merge

| Priority | Location | Notes |
| --- | --- | --- |
| 1 (low) | `<package>/modes/` | Bundled with the package: `plan`, `reviewer`, `explorer`. |
| 2 | `<agentDir>/modes/` | `$PI_CODING_AGENT_DIR` (default `~/.pi/agent`). |
| 3 (high) | `<project>/.pi/modes/` | Trust-gated — never loaded in an untrusted project. |

Later source wins on name collision. Missing directories are not errors.
