Course · Pi · Context
.pi/SYSTEM.md and APPEND_SYSTEM.md - write stash's conventions once
You’ve resumed sessions, forked a decision and kept one branch, and watched a long cleanup get compacted mid-run. All three protected the same thing: the conversation - the transient record of what you and Pi worked out together.
None of them touch the things about stash (the course’s running example, a small FastAPI feed reader) that were true before any conversation started and stay true after it ends: storage goes through the standard library’s sqlite3, outbound HTTP goes through httpx and never requests or urllib, tests run with pytest, pydantic models live in stash/models.py, feed dates get normalized on the way in.
You know those rules because they’re written down - the friend left a SYSTEM.md at the repo root, and you’ve read it. Here’s the problem, and it’s the kind this course keeps finding: Pi never reads that file. The context-file walk looks for AGENTS.md (or CLAUDE.md), not SYSTEM.md; the SYSTEM.md Pi does load has to live under .pi/ in the project or ~/.pi/agent/ globally.
Five conventions have been sitting in the repo since before your first session, and every session this week started blind to them. Each time one mattered - “run the tests”, “use httpx” - you typed it into the thread yourself, because that was the only channel the rules had.
Here’s the claim this lesson earns, worth stating before you see it happen: tomorrow morning, on a session about something that has nothing to do with today’s work, Pi will already reach for httpx and pass over requests - not because you say a word about it, but because of a file you’re about to write once, right now. (A convention loaded every session is a strong voice, not a guarantee - the honest version of the claim, and the reason hooks exist.)
Where the default prompt already reads project context
Section titled “Where the default prompt already reads project context”Pi’s default system prompt is a template assembled fresh every run, not a static string - and one of the steps in that assembly is a <project_context> block, filled in from context files Pi discovers by walking the directory tree: the global ~/.pi/agent/AGENTS.md, then each parent directory from the top down to your current one.
All matching files are concatenated in that order, and where two files say different things, the closer one lands later in the string - ordering you can verify in the printed prompt (ctx.getSystemPrompt(), the observability lesson’s call), not precedence you have to take on faith.
Drop an AGENTS.md in stash’s root and it’s injected into every session automatically. That’s already a form of writing it down once, and it’s worth knowing it exists (it’s what the site’s Rules chapter covers in general, spec-level terms).
But AGENTS.md is one voice among several in the assembled prompt - helpful context, not necessarily something the model treats as non-negotiable. For rules you want stated with more force - the kind you’d be genuinely unhappy to see quietly ignored - Pi gives you a sharper tool: the system prompt itself. Five layers exist between “a file on disk” and “the exact prompt sent this turn,” and they don’t all do the same job:
| Layer | Scope | Effect on the default prompt |
|---|---|---|
| Context-file walk | Global, then every parent directory down to the current one | Discovers files; doesn’t touch the template itself |
AGENTS.md (what the walk finds) |
Global or any directory on the path | Injected into <project_context> - additive, one voice among several |
APPEND_SYSTEM.md |
Project (.pi/) or global (~/.pi/agent/) |
Appends to the default template - additive, stronger voice |
SYSTEM.md |
Project or global | Replaces the default template wholesale |
CLI flags (--system-prompt, --append-system-prompt) |
Single invocation | Same two behaviors as above, no file needed |
APPEND_SYSTEM.md: add to the default, don’t replace it
Section titled “APPEND_SYSTEM.md: add to the default, don’t replace it”The move is small: a .pi/ directory in the repo root, the friend’s file copied in as .pi/APPEND_SYSTEM.md, lightly tightened on the way. First, the original, exactly as the friend left it:
# stash - system prompt
- Use SQLite (via the standard library `sqlite3`) for all storage.- Use `httpx` for all outbound HTTP; never `requests` or `urllib`.- Test with `pytest`.- Models are pydantic; keep them in `stash/models.py`.- Feed dates arrive in mixed formats and timezones - normalise on the way in.Five rules, one line each, the whole file. Here’s the same five where Pi will actually load them:
## stash conventions
- Use SQLite via the standard library `sqlite3` module for all storage.- Use `httpx` for all outbound HTTP; never `requests` or `urllib`.- Test with `pytest`.- Models are pydantic and live in `stash/models.py`.- Feed dates arrive in mixed formats and timezones. Normalize them on the way in (`stash/extract.py` parses them); stored rows must keep their timezone offset.The tightening is honest work, not formatting. Take the last rule: “normalize on the way in” is the invariant your first fix restored - parse_date was dropping timezone offsets until you repaired it. Written down here, the rule isn’t new information; it’s the invariant made standing, so every future session - yours, or the next person who inherits stash - holds the line even on a morning nobody remembers the bug.
Naming stash/extract.py turns a rule into a pointer, which costs three words and saves a search.
Commit it as one change: the new .pi/APPEND_SYSTEM.md in, the root SYSTEM.md out. The rules get exactly one home, and nobody - including you, three weeks from now - has to wonder which of two files is the live one.
This is the append option precisely because it’s additive and low-risk: you keep everything Pi’s default prompt already does well - the tool guidelines, the pointer to its own docs, the lazily-loaded skills metadata - and you’re only ever adding to it, never silently losing default behavior you didn’t mean to touch.
Notice what you just did to your own budget, though. Every line in that file is now permanent rent, not a one-time toll. Run wc -w .pi/APPEND_SYSTEM.md and the block above weighs 63 by the same count - that’s the tax on every single session from here on, stacked on top of the tools and the default template you already can’t shrink. Five rules today is cheap.
The fix that stops you re-explaining stash is also a new, permanent line in every session’s bill - you just moved the cost from your typing to the window. Keep it lean the same way you’d keep any other always-loaded file lean: worth its rent, or it doesn’t belong there.
Start a brand-new session tomorrow, on a task that has nothing to do with this week’s, and watch it already know. The exchange below is illustrative, but it’s the shape this file buys:
$ pi> add a route that re-fetches just one feed on demand
Sure - a POST /feeds/{feed_id}/resync in stash/main.py, reusing fetch_feed for the HTTP (httpx, per the conventions) and saving through db.save_article. Should it return the saved count like sync does?Nobody said a word about httpx in that session. The rule was already there when the session started - exactly the claim from the top of this lesson, and now you’ve watched it hold. That route is worth building for real, by the way; it comes back in a later chapter as a custom tool rather than a route.
SYSTEM.md: when you want a different agent, not an addition
Section titled “SYSTEM.md: when you want a different agent, not an addition”There’s a second file, .pi/SYSTEM.md (or ~/.pi/agent/SYSTEM.md globally), and it does something more drastic: it replaces the default prompt template wholesale, rather than adding to it. Context files and APPEND_SYSTEM.md still layer on top of whatever SYSTEM.md produces - the context-file walk is a layer, not part of the base template, which is why <project_context> survives a wholesale replacement - but the built-in identity line, tool guidelines, and doc pointers are gone unless you write them back in yourself.
That’s the right tool when you want a fundamentally different agent, not a stricter version of the same one. A stash-review agent that only ever reads and reports - flagging raw SQL against stash.db, or a stray import requests in a diff - is a SYSTEM.md job: replace the whole prompt with a narrower one that never mentions or invites writing or editing. Be clear about what that buys: the narrower prompt steers, it does not disarm - the write and edit tools are still technically there unless you also scope them away (--tools at spawn time, or a tool_call gate). A prompt-level boundary is guidance with a strong voice; the enforced version is tool configuration.
For the day-to-day conventions you carry across every real session, APPEND_SYSTEM.md is the one you’ll reach for. Keep the distinction straight: append to extend the same agent, replace to build a different one.
Both are also available as one-off CLI flags - --system-prompt <text> and --append-system-prompt <text> - for a single invocation without touching a file at all, which is exactly how a scripted or child pi process gets shaped later in this course.
What this chapter didn’t give you
Section titled “What this chapter didn’t give you”Everything in this chapter is context you wrote: AGENTS.md, APPEND_SYSTEM.md, SYSTEM.md, all loaded the same way on session one and session four hundred, never updated by Pi itself. That’s a real, working answer to “stop re-explaining yourself” - and it’s a different primitive from agent-managed memory, where the model decides on its own, mid-session, that something is worth keeping for next time.
Core Pi doesn’t ship that second kind. What exists is a community-extension pattern - a /memory command some Pi users have built that toggles a project memory.md file the agent can update - not something built into the harness. If you want Pi to notice a fact worth keeping without you deciding to write it down, that’s outside what APPEND_SYSTEM.md does, and outside this chapter.
What you’ve actually built
Section titled “What you’ve actually built”A week into stash, you no longer re-explain anything that matters. Sessions resume the conversation; compaction carries the long jobs; APPEND_SYSTEM.md carries the rules that have to survive every conversation, resumed, forked, or brand new.
The context window is a resource you spend and reclaim turn to turn, but the handful of things that are always true about stash don’t belong in that turn-to-turn spend at all - they belong on disk, read once at the start of every session, outside the per-turn bill. Free the first time you write them. After that, as you just measured, they’re a fixed cost like every other layer this chapter has been counting.
Put the who-decided question - the one the observability lesson taught you to ask of anything entering the window - over the layers this chapter added. The session file on disk costs nothing in the window until you resume or fork it, and it removes re-explaining a decision you already made: your call. Compaction’s reserve and protected tail - 16,384 tokens reserved for the reply plus 20,000 kept verbatim, 36,384 spoken for before a turn runs (the compaction lesson traces where both numbers come from) - remove a turn getting cut off mid-answer for lack of room: Pi’s defaults, yours only if you edit reserveTokens / keepRecentTokens in settings.json. And APPEND_SYSTEM.md, every line of it retaxed on every session forever - wc -w your own file to see the toll, against never retyping the friend’s five conventions by hand again: you wrote it, you pay for it.
Notice the split. The window’s fixed costs that shipped with Pi are Pi’s call; the three layers this chapter added are yours. That split is about to matter more than it has so far, because every layer above still runs through the same model, at the same depth, whether the task was choosing a dedup strategy or re-syncing a feed for the fortieth time.
You wrote stash’s conventions down once - and now you pay for them, and for the reserve, and for the tools, every single session, on the same expensive model regardless of what the session actually needs. Next: route the right model, at the right depth, to the right job.