# Pion
**The Pi coding agent, inside Obsidian โ with two fully isolated modes.**
A ๐ study coach you can configure for any subject, and ๐ป your daily coding agent โ
sharing credentials, never sharing context.
Plugin id `pion` ยท repo `obsidian-pion` ยท not to be confused with the unrelated `pi-agent`
plugin in the community directory.
[](https://github.com/Solren-zhen/obsidian-pion/actions/workflows/ci.yml)
[](LICENSE)

---
Pion mounts the [pi](https://github.com/earendil-works/pi) coding agent into a side panel.
pi already has tools, sessions, compaction and model configuration โ Pion does not
reimplement any of that. It renders pi's output as an Obsidian-native chat UI and adds one
thing pi cannot do on its own: **a second, completely isolated persona for studying.**
## Why two modes
The naive way to build a study agent is to write "you are a tutor" into the project's
`AGENTS.md`. That fails for a subtle reason: pi discovers `AGENTS.md` files by walking up
from the working directory, and loads skills from your global config. **A study persona in
either place is loaded on every daily coding session** โ burning context and nudging
behaviour you did not ask for.
Pion instead materialises the study persona in a hidden, vault-local profile and points pi
at it with two environment variables:
```
PI_CODING_AGENT_DIR -> /.obsidian/pi-study
PI_CODING_AGENT_SESSION_DIR -> /.obsidian/pi-study/sessions
```
Credentials (`models.json`, `auth.json`, `models-store.json`) are **symlinked** back to your
real config, so the persona is isolated while API keys stay shared and are never copied.
**The practical result:** being in study mode costs your coding setup exactly nothing, so
the study agent can stay on without you having to remember to turn it off.
| | ๐ Study | ๐ป Coding |
|---|---|---|
| Persona | Study coach (`AGENTS.md` + 5 skills) | Your existing pi config, untouched |
| Config dir | `/.obsidian/pi-study` | `~/.pi/agent` |
| Session history | `โฆ/pi-study/sessions` | `~/.pi/agent/sessions` |
| Panel conversations | separate list | separate list |
| Credentials | shared via symlink | as-is |
## What the study coach actually does
It is not a chat window that talks *about* studying. It is built around one failure mode:
LLMs love to explain everything at once, which feels productive and teaches nothing.
The persona is therefore a **behavioural contract**, not role-play:
- **Retrieval before explanation.** Quiz first; only explain after a real attempt.
- **One question at a time.** Never a whole practice paper at once.
- **Hint before reveal.** If you miss it, you get a nudge and a retry โ not the answer.
- **Verification after every explanation.** An unchecked explanation did not happen.
- **Something must be written to disk each round**, or nothing was learned.
Questions and explanations follow five **evidence-based principles** โ retrieval practice,
spacing, interleaving, concrete examples, dual coding โ with at least two required per
interaction. Gaps are written back into your own notes as mistake notes, reusing your
existing frontmatter fields, so the next session has real signal to prioritise with.
### Skills
| Skill | Trigger |
|---|---|
| `quiz-me` | "quiz me", "test me" โ interleaved questions from your real notes |
| `review-notes` | "review", "what should I review" โ spaced review ordered by weakness |
| `teach-concept` | "explain", "I don't get it" โ intuition โ definition โ example โ edge case โ check |
| `make-cards` | "make flashcards" โ converts notes to spaced-repetition cards |
| `study-plan` | "study plan", "how much is left" โ plan from time left and measured mastery |
### Presets
Any subject works; presets give a starting point, and every field can be overridden.
| Preset | For |
|---|---|
| `general` | anything โ the coach explores your vault and adapts |
| `kaoyan408` | the Chinese CS postgraduate exam (408 + Maths I) |
| `language` | vocabulary/grammar, JLPT/IELTS style goals |
| `programming` | learning a language or working through exercises |
Presets define the goal, note folders, mistake book, template, mastery field and values, and
the persona language (`zh` / `en`). Card separators are **auto-detected** from your
[obsidian-spaced-repetition](https://github.com/st3v3nmw/obsidian-spaced-repetition) config.
Existing generated files are never overwritten, so your edits survive upgrades.
### Panel tools (study mode)
Two toolbar buttons appear only in study mode:
- **็ๆๅคไน ๆธ
ๅ** โ the plugin itself reads the mastery field of your notes (Obsidian's
in-memory metadata cache; no extra processes) and computes the review queue for you:
weak statuses first, stalest next, capped at the top 15 with a per-status tally. The
list lands in your input box as a ready-to-send prompt, so scheduling is done by code
and the coach only has to teach.
- **ๅฏผๅบๅญฆไน ่ฎฐๅฝ** โ appends the current conversation (trimmed, plus the current weak
spots) as a timestamped section to a dated ๅญฆไน ่ฎฐๅฝ note under your configured ๆฅๅฟ/่ฎกๅ
directory. Learning results land in the vault instead of staying in the panel's own
data.json.
Both features write exclusively inside your vault; nothing in `~/.pi` is touched.
## Coding mode
Your normal pi, rendered in Obsidian:
- Streaming text with a live caret, plus collapsible thinking
- Tool cards grouped per turn, keyed by `callId` so parallel calls never cross wires
- Real unified diffs from `result.details.diff`, opened automatically for edits
- Session management โ each conversation keeps its own pi `--session-id`, resumable after restart
- `@`-mention notes (paths with spaces quoted automatically), token/cache usage, `Esc` to abort
- Optional send-time confirmation, and backends for `pi` / `claude` / `codex` / custom commands
## Requirements
- Desktop Obsidian (`isDesktopOnly`)
- A working [pi](https://github.com/earendil-works/pi) install with a configured model
(`pi --list-models` should list something)
## Install
**Manual** โ copy `main.js`, `manifest.json` and `styles.css` into
`/.obsidian/plugins/pion/`, then enable **Pion** in Settings โ Community plugins.
**From source**
```bash
git clone https://github.com/Solren-zhen/obsidian-pion.git
cd obsidian-pion
npm run verify # builds main.js, then runs the checks
```
`main.js` is a **generated bundle** โ `npm run build` inlines `src/study-profile.js` into it.
This is required, not optional: Obsidian only downloads `main.js`, `manifest.json` and
`styles.css` from a release, so a plugin that `require()`s a sibling file at load time
would crash for everyone installing from the community directory. There is still **no
bundler and no dependencies**; the build is a ~120-line script whose only job is that
inlining. Edit `src/*.js`, never the generated `main.js`.
## Setup
1. Settings โ **Pion**, confirm the pi executable (default `pi`). If it is not on `PATH`, add
its directory under *PATH additions*.
2. Leave provider/model empty to inherit your pi defaults.
3. Under **Study persona**, pick a preset and fill in only what differs.
4. Click **Set up / repair study profile**, then switch to ๐ in the panel toolbar.
## Data and privacy
| What | Where | Notes |
|---|---|---|
| Plugin settings + panel transcripts (both modes) | `/.obsidian/plugins/pion/data.json` | plain JSON, whitelisted fields only |
| Study persona + skills | `/.obsidian/pi-study/` | generated once, then yours to edit |
| Study session history | `/.obsidian/pi-study/sessions/` | fully separate from daily use |
| Coding session history | `~/.pi/agent/sessions/` | your daily pi; Pion never writes here in study mode |
| Model credentials | `~/.pi/agent/*.json` | **symlinked, never copied or read** |
### Disclosures
Pion is a front-end for an external program, so the directory review flags three behaviours.
They are inherent to the design rather than accidents, so rather than ask you to trust them,
here is exactly what each one does and how to check it. (The review rates these as **warnings**
and a **recommendation**, which per the directory docs do not block publication.)
#### Filesystem access outside the vault โ *warning*
The Node `fs` module can touch any file the process can reach. What this plugin actually does
with it is deliberately narrow โ **21 call sites** in the whole codebase, and only these methods:
```
existsSync (10) writeFileSync (4) unlinkSync (3)
lstatSync (1) mkdirSync (1) readFileSync (1)
readlinkSync (1) symlinkSync (1)
```
There is no `readdirSync`, no `rm`, no `rmdir`, and no recursion over your vault.
| Where | What | Why |
|---|---|---|
| `~/.pi/agent/` | `existsSync`, `lstatSync`, `readlinkSync` | check your credentials exist before linking; detect stale links |
| `/.obsidian/pi-study/` | `mkdirSync`, `symlinkSync`, `writeFileSync`, `existsSync`, `unlinkSync` | create and repair the isolated study profile |
| `/.obsidian/plugins/obsidian-spaced-repetition/data.json` | `readFileSync` (1 site) | read-only: detect your flashcard separators |
| plugin directory | `writeFileSync` (1 site) | materialise the tool-approval extension `pi-approve.js` (fixed content built into the plugin) |
- **Credential contents are never read or copied.** Only a symlink is created, and a symlink
stores a path, not data. This is what lets you avoid configuring API keys twice.
- **The single `readFileSync` is the only read of file *contents* anywhere**, and it targets
the spaced-repetition plugin's config so card syntax matches your setup. Everything else is
existence checks and writes.
- **Writes are confined to the isolated study profile** (`settings.json`, `AGENTS.md`,
`skills/*/SKILL.md`), plus the plugin's own `data.json` through Obsidian's API.
- **All three deletions only ever target files this plugin generated** โ `AGENTS.md` and its own
`skills/*/SKILL.md` โ and only when you press *Regenerate persona*. Your own files and
`settings.json` are never removed.
- No path is derived from file contents, and nothing is executed based on what is read.
#### Shell execution โ *warning*
Pion runs your local agent as a subprocess. Specifics:
- Only `spawn()` with an **argv array**. There is no `exec()`, no `spawnSync`, and no
`shell: true`, so nothing is passed through a shell โ no word splitting, no globbing, no
quoting bugs, no shell injection surface.
- The executable comes from **your settings** (default `pi`), and the argv is built from your
prompt and your settings. Nothing is executed that you did not type.
- The subprocess runs with your user's permissions, and the agent's own tools (`bash`, `write`,
`edit`) can then act on whatever you granted via the tools setting. **That is the point of the
plugin** โ it is an agent front-end โ and it is also why *require approval* exists as a
send-time confirmation gate, plus โ in the default **RPC transport** โ a per-tool-call
approval modal (see below).
- **RPC transport (default for the pi backend).** One long-lived `pi --mode rpc` child process
is kept per conversation while you chat, talking JSONL over stdin/stdout. Closing the panel,
switching conversations, or unloading the plugin closes the child's stdin, which pi treats as
a graceful shutdown; a SIGKILL fallback exists for a hung child. The child inherits your
environment and runs with your user's permissions, exactly like the one-shot mode it
replaces. The older one-shot transport (`--mode json`) remains selectable in settings.
- **Tool-approval extension.** With RPC, the plugin writes a small file `pi-approve.js` (its
content is fixed, built into the plugin) into the plugin's own directory and loads it into
the child with `pi -e`. The extension's only job: for tools matching your *ๅทฅๅ
ทๅฎกๆน* setting
(default `bash,write,edit`), route the call through a confirmation dialog that surfaces in
Obsidian as a native allow/deny modal. It adds no capabilities the child does not already
have; it only adds a gate. Set the option to ๅ
ณ้ญ to not load it at all.
#### Clipboard access โ *recommendation*
- One call site, reached only by clicking the copy button on a code block.
- **Write-only**: it copies text you are already looking at. The clipboard is never read, so
nothing you copied from outside Obsidian can be exposed.
#### Network use
- The plugin makes **no network requests of its own** โ no telemetry, analytics, ads, accounts,
or update pings.
- Requests to your model provider happen inside `pi`, under your pi configuration.
#### Release provenance
Release assets carry GitHub build-provenance attestations, so you can cryptographically verify
that `main.js` was built from the tagged source in CI rather than on someone's laptop:
```bash
gh attestation verify main.js --repo Solren-zhen/obsidian-pion
```
#### Verify the above yourself
Every claim here is asserted in the offline test suite, which needs no Obsidian and no network:
```bash
npm test # includes checks for write-only clipboard, no eval, no console logging,
# no innerHTML, no !important, and the presence of these disclosures
```
## Architecture
```
main.js GENERATED bundle โ the only JS file Obsidian installs
src/
main.js plugin + chat view + settings + backend bridge + mode isolation
study-profile.js preset/config โ persona + skills rendering, isolated profile setup
styles.css
dev/ offline verification tooling (never loaded by Obsidian)
build.js inlines src/study-profile.js into main.js (no bundler)
minidom.js minimal DOM + Obsidian API stub
test.js 62 assertions over replayed real pi event streams
e2e.js real pi subprocess
e2e-study.js study mode: persona loads, daily pi untouched
check-manifest.js manifest/versions consistency
check-privacy.js blocks personal data and secrets from being published
```
Plain CommonJS, **zero dependencies, no bundler**. Sources are ~1.7k readable lines; the
generated bundle is verified in CI to load standalone (test 15 simulates a fresh install
with only the three shipped files present).
## Development
```bash
npm run build # regenerate main.js from src/
npm run verify # build + syntax + 62 offline assertions
npm run e2e # real pi subprocess (consumes tokens)
npm run e2e:study # study mode end-to-end (consumes tokens)
```
The offline suite replays **captured real pi event streams** through the state machine with a
hand-rolled DOM stub, so it needs neither Obsidian nor a network โ and it runs in CI on Node
18/20/22. It covers parallel tool calls, thinking-before-text, throttled-stream commit
ordering, multi-view duplication, failure paths, persistence purity, migration idempotency,
study/coding isolation, preset genericity and **distribution packaging**. See
[`CONTRIBUTING.md`](CONTRIBUTING.md) and [`dev/README.md`](dev/README.md).
## Acknowledgements
- [pi](https://github.com/earendil-works/pi) โ the agent doing the actual work
- [obsidian-spaced-repetition](https://github.com/st3v3nmw/obsidian-spaced-repetition) โ card
syntax Pion targets so notes stay reviewable
- [classbuild](https://github.com/jtangen/classbuild) โ the formalised set of evidence-based
learning principles the coach is built around
- [LearnKit](https://github.com/ctrlaltwill/LearnKit) and
[Mr. Ranedeer](https://github.com/JushBJJ/Mr.-Ranedeer-AI-Tutor) โ workflow and
personalisation inspiration
## License
[MIT](LICENSE)