---
slug: "danypops-pi-profiles"
source_type: "readme"
source_url: "https://cdn.jsdelivr.net/gh/DanyPops/pi-profiles@main/README.md"
repo: "https://github.com/DanyPops/pi-profiles"
source_file: "README.md"
branch: "main"
---
# @danypops/pi-profiles

A [pi](https://pi.dev) extension for named profiles: bundle a model, thinking level, active tools, and system-prompt instructions behind one name, and switch between them with `/profile`, `--profile`, or `Ctrl+Shift+U`.

Built directly on pi-coding-agent's own bundled `preset.ts` example: same config-merge, CLI flag, selector UI, and cycling pattern. It adds one thing that example doesn't do, an `allowedModels` soft guard that warns if you manually switch to a model outside the active profile's expected set. That matters when profiles map to separate billing, for example a personal OpenRouter/OpenAI budget versus a work Google Cloud project paying for Claude via Vertex AI.

## Not to be confused with `pi-profiles` (npm, unscoped)

There's an unrelated, unscoped npm package also called `pi-profiles` ([chaychoong/pi-profiles](https://github.com/chaychoong/pi-profiles), CLI: `ppi`). It solves a different, complementary problem: it wraps `PI_CODING_AGENT_DIR` to give you fully isolated `~/.pi/profiles/<name>/` config directories (separate settings, extensions, skills, auth, and sessions per profile), launched via `ppi use <name>`. Its own docs are explicit that it's a *different layer*: "No mid-session model/tool switching. Pi's built-in `/model` command handles that."

Use `ppi` for a hard wall (see below). Use this extension for fast, in-session switching once you're already running pi. They compose fine together.

## What this is *not*

This does not hide providers, disable extensions, or enforce a hard boundary. Pi has no supported extension API to unload another extension or restrict which providers/models are selectable (see the [`allowedModels`](#allowedmodels-the-soft-guard) section below). If you need a *hard* separation (distinct credential stores, distinct session history, no way to accidentally cross-select), use [`ppi`](https://github.com/chaychoong/pi-profiles) (or hand-roll the same idea with pi's own `PI_CODING_AGENT_DIR` env var pointing at two entirely separate config directories), or project-local `.pi/settings.json` if the split maps cleanly to different working directories.

## Install

Local, not published:

```bash
pi install ~/Projects/pi-profiles
# or just for one run:
pi -e ~/Projects/pi-profiles/index.ts
```

## Config

Merged from (project overrides global):

- `~/.pi/agent/profiles.json` (global)
- `<project>/.pi/profiles.json` (project-local)

```json
{
  "personal": {
    "provider": "openai",
    "model": "gpt-4.1",
    "thinkingLevel": "medium",
    "allowedModels": ["openai/*", "openrouter/*", "google/*"],
    "theme": "blossom"
  },
  "work": {
    "provider": "anthropic-vertex",
    "model": "claude-sonnet-5",
    "thinkingLevel": "high",
    "allowedModels": ["anthropic-vertex/*"],
    "instructions": "Billed to the work GCP project. Keep usage scoped to work tasks.",
    "theme": "industrial"
  }
}
```

A starter file matching this repo's own personal/work split (`openai` vs. `anthropic-vertex`) was seeded at `~/.pi/agent/profiles.json`; edit models/providers to match what you actually have configured (`pi --list-models`). `theme` names any theme pi already knows about: built-in (`dark`/`light`), from another installed package, or your own files in `~/.pi/agent/themes/*.json`. This package doesn't ship any themes itself; `blossom` and `industrial` above are both plain files in `~/.pi/agent/themes/`.

### Fields

| Field | Type | Effect |
|---|---|---|
| `provider` + `model` | string | Resolved via `ctx.modelRegistry.find()` and applied with `pi.setModel()` |
| `thinkingLevel` | `off\|minimal\|low\|medium\|high\|xhigh\|max` | `pi.setThinkingLevel()` |
| `tools` | string[] | Replaces the active tool set via `pi.setActiveTools()`; unknown tool names are warned and dropped |
| `instructions` | string | Appended to the system prompt for the rest of the session while the profile is active |
| `theme` | string | Name of a theme registered with pi (see `ctx.ui.getAllThemes()` / `/settings`); applied with `ctx.ui.setTheme()`. Unknown names are warned and skipped |
| `allowedModels` | string[] (glob, matched against `provider/model`) | Advisory only, see below |

### `allowedModels`, the soft guard

While a profile is active, switching models by hand (`/model`, `Ctrl+P` cycling) to something outside `allowedModels` triggers a warning notification. It does **not** block the switch. `model_select` is a notification-only event in pi's extension API, and there's no supported way to veto it. Its only job is to catch the "forgot which profile was active" slip before you burn tokens against the wrong account.

Switches the profile itself makes (via `/profile <name>` or `--profile`) never trigger the warning, and restoring a model from a saved session (`event.source === "restore"`) is ignored too.

## Usage

```bash
pi --profile work                 # start with the "work" profile applied
```

```
/profile                          # interactive selector (also shows "(none)" to clear)
/profile work                     # switch directly
Ctrl+Shift+U                      # cycle through all defined profiles, then "(none)"
```

The active profile name is shown in the footer status bar and persisted across turns in the session (so it survives `/reload` and session resume), but model/tools/thinking/theme are only re-applied on `--profile`/`/profile`/cycling. They are not silently re-applied on session restore, since the session already has whatever was active when it was saved. Picking `(none)` restores whatever model/tools/thinking/theme were active before the first profile was applied.

### Remembering your last profile across restarts

Separately from session persistence above, the last profile you explicitly activated (`--profile`, `/profile <name>`, the selector, or cycling) is also written to `~/.pi/agent/profiles-last.json`. The next brand-new pi session (no `--profile` flag, no prior session to resume) reapplies it automatically, including model, tools, thinking level, and theme, with a notification (`Profile "work" restored from last session`). Picking `(none)` clears it, so the next fresh session starts with no profile again.

This is global only, not per-project, matching where the global half of `profiles.json` already lives. If you work across multiple unrelated projects and want different "last profile" memory per project, that's not supported; use project-local `profiles.json` for the profile *definitions* and `--profile` explicitly per invocation instead.

## Development

```bash
npm install
npm run check    # tsc --noEmit
```

Iterate without reinstalling:

```bash
pi -e ~/Projects/pi-profiles/index.ts
```

## License

MIT
