XJTLUmedia

protect-context · Agent Guardrail Suite

Community XJTLUmedia
Updated

keeps the rules, decisions, and proof-of-work an agent needs in front of it

protect-context · Agent Guardrail Suite

Durable context, real evidence, and a brake pedal for AI coding agents.

protect-context keeps the rules, decisions, and proof-of-work an agent needs infront of it — then makes scope creep, unverified claims, and repeated failuresvisible before they cost you a session.

protect-context injects a session brief — contract, decisions, corrections, loops, last evidence — before the agent starts working

Terminal recording of guard_verify: two failures of false, the third attempt blocked, then a masked || true exit 0 labelled UNTRUSTED

One project-local .agent-guard/ store · three interfaces backed by the samelogic:

Interface Use it for Runtime
pi extension Automatic brief injection, status widget, loop blocking, diff warnings, risky-command confirmation TypeScript, loaded by pi via jiti
MCP server Portable model/host access to the same tools Python 3.10+, fastmcp
Python CLI People, scripts, CI, and hosts without MCP Python 3.10+

The suite is advisory-first. It records evidence and makes unsafe orrepetitive behavior visible; it is not a sandbox and does not prove that achange is correct. SPEC.md is the frozen v1.0 contract.

Architecture: the pi extension, MCP server, and Python CLI share a project-local .agent-guard store for rules, journal entries, and evidence; only pi injects briefs automatically

Contents

  • Why this exists
  • What it does
  • Quick start
    • Install the pi extension
    • Install the MCP server
      • Cursor / Claude Code / any stdio host
      • From a checkout
    • Install the Python CLI
    • From source
  • See it work
  • A 30-second tour
  • Interface comparison
  • Reference
    • pi commands and tools
    • MCP tools
    • CLI
    • Configuration
    • Environment variables
    • Runtime store and concurrency
  • The 12 failure modes and what ships
  • How it works
    • Session lifecycle
    • The brief
    • Local recall
    • Evidence and loop detection
    • Scope audit
    • Trust and provenance
    • Context budget
  • Development and validation
  • Troubleshooting
  • Limitations and security boundary
  • Design sources
  • Non-goals
  • License

Why this exists

Long agent sessions fail in boring, expensive ways:

  • the constraints you stated at minute two are gone by minute forty;
  • "done" is claimed with no command output behind it;
  • a "quick fix" quietly rewrites six unrelated files;
  • decisions get re-litigated because nothing recorded the reasoning;
  • the same failing command is retried until the context window dies.

protect-context treats those as state problems, not intelligence problems.A durable store plus three small habits — brief before work, evidence afterwork, journal when something is decided — removes most of the failure surfacewithout turning the agent into a different agent.

What it does

Primitive What it gives you Where you meet it
Brief Owner task, contract, recent trusted decisions and corrections, a short unverified section for the agent's own notes, open loops, and the last evidence ID — injected before a turn and re-asserted mid-run after a compaction Automatic in pi; orientation, status, guard_journal elsewhere
Orientation Bounded repo digest: tree, languages, and conventions files (CLAUDE.md, AGENTS.md, CONVENTIONS.md, README.md, README) Injected once per digest in pi; orientation in CLI/MCP
Evidence ledger Every verification run recorded as ev-NNNN with exit code, duration, command, and a bounded output digest guard_verify, protect-context verify, guard_evidence
Loop brake Identical failing commands are fingerprinted; the Nth attempt is blocked instead of re-run Hard block in pi and the CLI; reported through MCP
Scope audit git diff stats over tracked, staged, and bounded untracked work, with ok / warn / hard verdicts Widget and status in pi; diff, guard_diff_audit
Journal Durable decision / correction / risk / note entries with tags and trust levels guard_note, protect-context note, guard_journal
Recall Keyword, tag, type, evidence-id, and command search over the retained journal and evidence window — no embeddings guard_recall, protect-context recall; the brief names it when a cap hides entries
Risky-command confirm Nine catastrophic shell patterns get one grouped confirmation instead of none pi, when a UI is available
Preflight Tool presence and versions plus Git worktree detection preflight, guard_preflight
Context budget Watermarks on context usage and hard caps on every injected block Widget, budget, guard_budget

Quick start

Python 3.10+ is required for the CLI and MCP server. Node 22.19+ is requiredonly to validate the TypeScript extension from a checkout — pi supplies theextension's runtime modules, and the extension has zero npm runtimedependencies.

Install the pi extension

Installed from npm. The package declares "pi": { "extensions": ["./extension/index.ts"], "image": "./preview.png" } and the pi-packagekeyword required by the Pi package gallery:

pi install npm:protect-context

# Try it for one run without adding it to settings
pi -e npm:protect-context

Install it through pi, not through npm install. The extension declares pi'shost modules (@earendil-works/pi-ai, @earendil-works/pi-coding-agent,typebox) as "*" peer dependencies, as Pi requires; Pi suppresses peerinstallation for managed npm packages, while a plain npm install protect-context does not. The second path downloads the whole pi runtime andsurfaces advisories from that tree, which belong to the host and not to thiszero-runtime-dependency extension.

Remove any hand-installed copy first — a directory such as~/.pi/agent/extensions/protect-context or .pi/extensions/protect-context,or pi remove npm:protect-context if that copy came from npm. Two copiesregister the same tool names, and pi drops the second one:

Error: Failed to load extension "…/.pi/npm/node_modules/protect-context/extension/index.ts":
Tool "guard_verify" conflicts with /home/you/.pi/agent/extensions/protect-context/index.ts

One error is reported per tool (guard_verify, guard_note,guard_contract_check, guard_status, guard_journal, guard_recall), sonothing from the npm copy loads until the other copy is gone.

Or copy/symlink the checkout into a trusted extension directory:

# Personal
mkdir -p ~/.pi/agent/extensions
cp -r extension ~/.pi/agent/extensions/protect-context

# Project-local (read after the project is trusted)
mkdir -p .pi/extensions
cp -r extension .pi/extensions/protect-context

Start a session and you get a single-line status widget, a brief before thefirst turn, and five slash commands. No configuration file is needed: theextension seeds .agent-guard/config.json with the frozen defaults on firstsession start.

Install the MCP server

The MCP server is the PyPI package. With uv installed, uvx protect-context mcpserves it on stdio. After pip install protect-context, the same entry pointis protect-context mcp. Both run the protect-context console script(protect_context.cli:main); the mcp verb starts protect_context.mcp_server.

Cursor / Claude Code / any stdio host (recommended)

Cursor — project .cursor/mcp.json or global MCP settings:

{
  "mcpServers": {
    "protect-context": {
      "command": "uvx",
      "args": ["protect-context", "mcp"]
    }
  }
}

Claude Code — CLI:

claude mcp add protect-context -- uvx protect-context mcp

Or hand-edit Claude Desktop / Claude Code MCP config with the same command /args shape.

Generic .mcp.json (pi-mcp-adapter and other hosts). This repositorychecks in the same file at the root:

{
  "mcpServers": {
    "protect-context": {
      "command": "uvx",
      "args": ["protect-context", "mcp"]
    }
  }
}

Set "env": { "PC_SESSION_ID": "my-session" } when several host sessions shareone repo and need separate evidence and loop views.

The same uvx protect-context mcp command, with each host's current configpath, is copied in full under docs/integrations/:

  • Cursor
  • Claude Code
  • VS Code (Copilot agent mode)
  • Cline
  • Windsurf
  • Continue
  • Zed
  • Codex CLI
  • Gemini CLI
  • pi
From a checkout

An editable install uses that environment's interpreter:

{
  "mcpServers": {
    "protect-context": {
      "command": "/absolute/path/to/protect-context/.venv/bin/python",
      "args": ["-m", "protect_context.mcp_server"]
    }
  }
}

PC_SESSION_ID uses the same env object as the stdio examples above.

The MCP surface exposes the nine frozen v1.0 tools plus later read surfaces(guard_journal, guard_orientation, and guard_recall). Tools carryconservative MCP annotations; in particularguard_verify is marked destructive and open-world because it can run anyshell command. Annotations are host hints, not enforcement — configure the hostto require human approval for that tool.

Install the Python CLI

pip install protect-context      # or: pipx install protect-context
protect-context init             # seed .agent-guard/ in the current project
protect-context status
protect-context verify "pytest -q"
protect-context mcp               # serve the MCP tools on stdio

The CLI and MCP server are distributed through PyPI; the npm package ships onlythe pi extension. Both read the same .agent-guard/ directory, so a project canuse the extension in pi and scripted verification in CI at the same time.

From source

# Python package, CLI, MCP server, and Python tests
python3 -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"

# Reproducible extension development checks (dev dependencies only)
npm ci --ignore-scripts
npm run check

If the machine's default Node is older than 22.19, run the same checks with anephemeral Node 22 binary after npm ci:

npx -y node@22 node_modules/typescript/bin/tsc --noEmit -p extension/tsconfig.json
npx -y node@22 --experimental-strip-types --test extension/test/*.test.ts

A 30-second tour

Excerpts from a real session in this repository.

Ask for the current state:

$ protect-context status
PC status · session default · git 91fad4d
Δdiff +88/-8 · 6 files · verdict ok
loops: none
contract 12 constraints
conventions: README.md
Nudge: verify changes with guard_verify and cite the evidence id before claiming success.

Record proof instead of asserting it:

$ protect-context verify --timeout 180 npm run check
PC verify ev-0005 · exit 0 · ok · 1440ms
cwd: /path/to/project
cmd: npm run check
digest: …
Nudge: cite this evidence id when claiming success.

A pipeline or || true is still recorded with the shell's real exit code, butthe report says UNTRUSTED and the CLI exits 1. That exit 0 is not proof.

Re-run the same failing command a third time and the brake engages:

$ protect-context verify "npm test"
PC verify blocked · npm test · 3 prior failures
Nudge: change approach or gather new information — do not re-run unchanged.

What pi injects before the agent starts working (sections elided):

contract:
01-obey-instructions: Obey the owner's explicit instructions and stated constraints exactly; when instructions conflict, ask instead of guessing.
02-evidence-before-success: Never claim success without citing a fresh verification run (evidence id or pasted output) for the exact claim.
…
decisions:
- Use the atomic-mkdir lease, not fcntl alone; Python must interoperate with the extension. [locking,spec]
last verify: ev-0005 exit=0 ok 'npm run check' 1.4s

And the always-on one-line widget:

PC Δ+88L/6f 0loop ev5 ctx8%

Illustrative pi widget: Δ+123L/4f means 123 changed lines across 4 files, 1loop means one command at the blocking threshold, ev2 counts two verification runs this session, and ctx41% shows context usage

See it work

examples/loop-brake-demo/demo.py callsguard_verify in a temporary git repository. Two identical false runs arerecorded. The third is blocked and adds no evidence id. A different commandthat forces exit 0 with || true is stored as UNTRUSTED. The temporarydirectory name and the millisecond timings change every run. This is one run:

$ guard_verify('false')
PC verify blocked · false · 2 prior failures
Nudge: change approach or gather new information — do not re-run unchanged.

$ guard_verify("python3 -c 'raise SystemExit(1)' || true")
PC verify ev-0003 · exit 0 · UNTRUSTED · 16ms
cwd: /tmp/pc-loop-brake-4tqwi013
cmd: python3 -c 'raise SystemExit(1)' || true
digest: (no output)
why: The command masks failure (`|| true`, `|| :`, `; true`, or similar), so exit 0 does not prove the check passed.
fix: drop the trailing failure mask and re-run the command as-is. Do not cite this id as proof.
Nudge: this run is recorded, but exit 0 here is not evidence of success.

The full transcript, including the two recorded failures, is in thedemo README. From a dev checkout:

python3 examples/loop-brake-demo/demo.py

Interface comparison

Capability pi extension MCP server CLI
Brief injected automatically before a turn yes (change-triggered) — —
Orientation digest (tree, languages, conventions) injected once per digest guard_orientation orientation
Verification evidence (ev-NNNN) guard_verify, /guard-verify guard_verify verify
Loop blocking blocks the command guard_loop_check reports and advises verify refuses; loops exits 1
Scope audit session-start baseline + warnings guard_diff_audit (explicit baseline) diff --baseline REF
Journal write/read guard_note, /guard-note, guard_journal guard_note, guard_journal note, journal
Recall across the retained window guard_recall; brief may surface 1–2 trusted tag matches guard_recall recall
Trusted re-injection of decisions yes, after change and after every compaction (incl. mid-run) read views only read views only
Re-injection of the agent's own notes yes, in an explicitly unverified section read views only read views only
Owner task anchor /guard-task, auto-captured from the first prompt — —
Contract management /guard-contract guard_contract contract list|add|remove
Risky-command confirmation one grouped confirm host approval policy —
Status widget yes — status

Reference

pi commands and tools

Slash commands:

Command Action
/guard-status Compact session status: diff verdict, loops, evidence, contract, context
/guard-contract list · add <text> · remove <id> — edit the persistent contract
/guard-verify Run a command through the evidence ledger
/guard-note Journal a decision · correction · risk · note
/guard-task Show · set <text> · --clear the owner task anchor that the brief re-injects
/guard-report Full session summary for handoff or review

Model-callable tools:

Tool Arguments Notes
guard_verify command, timeout_s (default 180, clamped 1–900) Records ev-NNNN; refusal on a blocked loop
guard_note type, text, optional tags Forced to the agent trust level
guard_evidence limit (default 5), optional evidence_id Read-only; the window shows each non-ok row's last output line, and evidence_id returns one run in full
guard_journal limit (0–20, default 5), optional type Read-only; recovers decisions, corrections, and the agent's own earlier notes after a compaction
guard_recall query, type, tags (AND), limit (0–20, default 5), evidence_id, cmd Read-only scan of the retained window; hits labelled trusted or agent
guard_contract_check summary Deterministic keyword coverage checklist, never a proof
guard_status — Same data that feeds the widget, including journal totals

MCP tools

Tool Annotation class Purpose
guard_status local, initializing read Session status digest
guard_verify shell execution (destructive, open-world) Run a command, record evidence
guard_evidence read-only Recent verification entries
guard_contract local mutation list · add · remove
guard_note local append Journal an entry (tagged agent)
guard_journal read-only Recent journal entries, optionally filtered by type
guard_recall read-only Search the retained journal and evidence window by keyword, tag, type, evidence id, or command
guard_loop_check local, initializing read Failing-command streaks and advice
guard_diff_audit local, initializing read Diff stats and verdict vs thresholds
guard_preflight local, initializing read Tool/version and worktree check
guard_budget read-only Context-budget watermarks
guard_orientation read-only Repo digest and conventions files

CLI

Every command returns 0 for OK, 1 for a triggered warning or failedverification, and 2 for an input/environment error.

protect-context init
protect-context status
protect-context verify [--timeout 180] pytest -q
protect-context evidence [--limit 5]
protect-context contract list
protect-context contract add "Keep the public API stable" [--global]
protect-context contract remove ID [--global]
protect-context note correction "use rg, not grep" --tags search
protect-context journal [--limit 5] [--type decision]
protect-context recall [QUERY] [--type decision|correction|risk|note|evidence] [--tag TAG] [--limit 5] [--evidence-id ev-NNNN] [--cmd FINGERPRINT]
protect-context orientation
protect-context loops
protect-context diff [--baseline REF]
protect-context preflight
protect-context budget
protect-context mcp

Configuration

The extension seeds .agent-guard/config.json on first session start; initdoes the same for CLI/MCP-first use. Unknown, wrongly typed, fractional, orout-of-range values fall back to the frozen default.

Key Default Meaning
loop_block_after 3 Block the Nth identical failing command attempt
diff_warn_lines 400 Changed-line warning threshold
diff_hard_lines 800 Changed-line hard-level threshold
diff_warn_files 10 Changed-file warning threshold
ctx_warn_pct 70 First context-usage warning watermark
brief_mode "changed" Inject only when the brief digest changes; "always" injects every turn
brief_max_precedents 3 lesson precedents re-injected per brief (0–10; 0 disables the section)
risk_confirm true Ask once before a matching risky pi bash command when UI is available
preflight_tools git, python3, pip, node, npm, rg, pytest Executables checked by preflight

Numeric thresholds may be set to zero; the extension records an explicit firstnotification so a zero watermark does not silently disable alerts. Preflightchecks at most 20 configured tool entries. If more are configured, it lists theomitted count and returns a warning instead of claiming the environment isfully checked. Version probes have a two-second per-tool timeout, share a15-second overall deadline, and decode at most 4 KiB of output; a missingversion is reported as an incomplete preflight.

Making it yours: the personal layer

The twelve seeds and the config defaults are the same for everyone, on purpose:they are the floor that makes the guardrail comparable across machines. What isyours goes in a personal layer that every project inherits:

built-in defaults  <  ~/.config/protect-context/  <  .agent-guard/
                        contract.json                 contract.json
                        config.json                   config.json

The directory is $PC_USER_DIR, else $XDG_CONFIG_HOME/protect-context, else~/.config/protect-context. Write it from the owner surfaces:

$ protect-context contract add "Prefer stdlib over new dependencies" --global
added 13-prefer-stdlib-over-new-dependencies to personal layer
…

$ protect-context contract list          # every project sees it
personal layer: 1 from /home/you/.config/protect-context/contract.json (applies to every project; a project row with the same id or text wins)

The same flag works in pi: /guard-contract add <text> --global and/guard-contract remove <id> --global. Without it, writes go to the projectstore as before, and guard_contract (the agent-callable tool) never touchesthe personal layer.

Rules that make the layering predictable:

  • The project wins. A project row with the same id overrides the personalone, and a personal constraint identical in text to a project row is notlisted twice. Give a personal override the same id (e.g. 03-stay-in-scope) toreword a seed for every repo.
  • Defaults mean "no opinion". The extension seeds config.json with everydefault value; a project value equal to the built-in default therefore does notpin it, so your personal ctx_warn_pct still applies. Any different projectvalue is an explicit choice and wins.
  • Nothing is created behind your back. The layer is never seeded or repaired:a missing file means "no personal constraints", and an unreadable one isignored in memory and left untouched. Deleting a personal constraint requires--global; a project-scope remove says so instead of silently doing nothing.
  • Each layer has its own lease. A --global write locks the personaldirectory, not the project store, so two projects writing personal state cannotrace each other.

Sharing a contract with a team

.agent-guard/ is runtime state and normally gitignored. If a team wants oneshared owner contract, track only that file:

.agent-guard/*
!.agent-guard/contract.json

Runtime files (events, evidence, journal, session state) stay local, whilecontract.json becomes reviewable in pull requests. Personal constraints stillapply on top — each teammate's personal layer is weaker than the shared projectrow.

The nine catastrophic patterns that trigger the pi confirmation are frozen anddeliberately narrow, so benign lookalikes stay silent:

rm -rf / · rm -rf ~ · git push --force (not --force-with-lease) ·git reset --hard · mkfs … /dev/… · dd … of=/dev/… ·chmod -R 777 / · curl … | sh · sudo rm

Environment variables

Variable Effect
PC_SESSION_ID Names the session used for evidence and loop views, so parallel hosts do not share a view
PC_USER_DIR Overrides the personal-layer directory (default $XDG_CONFIG_HOME/protect-context or ~/.config/protect-context)

Runtime store and concurrency

Runtime files stay in the frozen .agent-guard/ layout and are gitignored:

config.json · contract.json · journal.jsonl · events.jsonl ·verify_runs.jsonl · state.json · verify_count.json (a stamp-verifiedevidence-count sidecar, discarded rather than trusted when the ledger moves)

The optional personal layer lives outside the repository at~/.config/protect-context/ (contract.json, config.json) and is neverwritten implicitly; see Making it yours above.

Ledgers bound themselves instead of growing forever: once events.jsonl,verify_runs.jsonl or journal.jsonl passes 1 MiB it rotates to<name>.1.jsonl, keeping at most three rotated segments and dropping the oldest.Reads concatenate the segments oldest-first, so recent rows and the highestevidence id always survive; budget and loop streaks describe the retainedwindow. Rotation is lesson-aware: journal rows tagged lesson are carriedinto the fresh live file when their segment is dropped, so a durable precedentcannot be deleted by size-based rotation (each lesson is copied once, and acopy that somehow fails never breaks the append that triggered the rotation). Session baselines are keyed per session inside state.json, so parallelpi sessions in one repo no longer overwrite each other's watermarks.

Python and TypeScript use the same atomic-directory lease at.agent-guard/.lock.d, with an owner token, PID/hostname checks, boundedwaiting, and conservative stale recovery. Python also takes its legacy localfcntl.flock inside that lease. Evidence-ID allocation, JSONL appends, andcontract read-modify-write operations hold the shared lease, preventingduplicate IDs and lost contract updates across runtimes. JSON file replacementuses unique temporary files.

The lease follows the atomic mkdir design used by mature lock libraries whileremaining dependency-free and interoperable with Python. A crashed same-hostowner is reclaimable after the stale interval; a stale foreign-host owner failsclosed because process liveness cannot be proved safely without an externalcoordinator. Recovery retains a small identity-bound .lock.d.stale.*directory as a durable race guard; it does not block new writers and livesinside the already-ignored runtime store. Contract mutations also fail closedwhen owner rows are malformed, preserving the original file for repair.

The 12 failure modes and what ships

# Failure mode Shipped mitigation Enforcement
1 Violating instructions Persistent contract; provenance-aware, change-triggered brief injection; seed-removal refusal at the MCP boundary Context/advisory
2 Claiming success without verification Verification ledger, evidence IDs, status/evidence views, failure digests Recorded evidence; not semantic proof
3 Editing too much / scope creep Git diff audit over tracked, staged, and bounded non-ignored untracked work; safe explicit baselines Warn/advisory
4 Misunderstanding the repo Worktree-root-aware bounded orientation digest in pi, CLI, and MCP; conventions-file discovery Automatic context + explicit read
5 Plausible but subtly wrong code Deterministic contract checklist, verification gate, test-required seed Heuristic/advisory
6 Losing earlier context Persistent journal; owner task anchor; recent trusted decisions re-injected after change and after every compaction (including mid-run threshold/overflow); agent notes re-injected as unverified; guard_journal read views; guard_recall searches the retained window Automatic context + explicit read
7 Repeating corrected behavior Bounded tagged corrections; trusted corrections re-injected; model-authored entries retained for owner review Automatic context + trust boundary
8 Large diffs Configurable line/file warning and hard-level notifications, status verdict, widget Warn/advisory
9 Repetitive debugging loops Shared bash/evidence streak reconstruction; third identical failing command blocked by default in pi, CLI, and MCP Hard block at configured threshold
10 Environment/tool failures Tool/version and Git-worktree preflight with warning exit outside a repository Diagnostic
11 Permission interruptions One grouped pi confirmation for the frozen risky-command patterns; environment preflight Interactive confirmation where UI exists
12 Token/API cost and latency Bounded brief sections, journal/contract text, evidence digests, untracked reads, output previews, and context watermarks Automatic budgeting/advisory

"Shipped" means the mitigation is reachable and tested. It does not turn anadvisory signal into a security or correctness guarantee.

How it works

Session lifecycle

The extension is a thin event layer over the shared store. Nothing leaves yourmachine: there are no network calls and no LLM calls in the guardrail logic.

Event What happens
session_start Seed config if missing, scan orientation, freeze the diff baseline, start the status widget
before_agent_start Build the brief; inject orientation and/or brief as a hidden message when their digests changed
tool_call (bash) Match the nine risky patterns → one grouped confirm; check the loop fingerprint → block at the threshold; record the normalized command, with ok=false when the call was refused
tool_execution_start / tool_execution_end Record bash outcomes and verify results into the shared loop state and evidence ledger
turn_end Refresh the widget: diff stats, open loops, evidence count, context percentage
session_compact Re-inject trusted context so compaction cannot silently erase it
session_shutdown Flush state and clear the widget

The brief

The brief is deliberately tiny — a persistent prompt that grows without bound isits own failure mode. Within the frozen 12-line constraint cap:

  1. newest owner constraints are selected first,
  2. then the frozen seed constraints,
  3. then other entries.

An agent-authored constraint therefore cannot crowd out a seed. Below thecontract it carries up to 3 recent trusted decisions, up to 3 trustedcorrections, up to 2 unverified corrections, up to 2 unverified agent notes,up to 2 open loops, and the last evidence ID with its trust label (ok,FAIL, UNTRUSTED), exit code, duration, and a truncated command. Correctionsthe model wrote get their own unverified slot so a newer ordinary note cannotstarve the record of what the owner corrected. When a caphides journal entries, the brief points at guard_recall. If the owner task'swords match a tag on a trusted entry that did not fit, it also injects at mosttwo of those matches. brief_mode: "changed" (default) injects only when therendered digest actually changed, so a quiet turn costs nothing.

Local recall

guard_recall (and protect-context recall) scans the retained window: thelive journal.jsonl and verify_runs.jsonl files plus the rotated segmentsthe other readers already concatenate. It does not embed text and it does notadd a vector index.

Filters combine with AND:

  • keywords — every word must occur as a case-insensitive substring ofjournal text, a verify command, or a verify digest. A word that is itselfev-NNNN also matches that evidence id (ev-7 and ev-0007 are the same id).
  • --type / type — decision, correction, risk, note, or evidence.
  • --tag / tags — every tag must be present. Matching is exact andcase-insensitive. Evidence rows have no tags, so a tag filter excludes them.
  • --evidence-id / evidence_id — one exact evidence id.
  • --cmd / cmd — the frozen command fingerprint, or a substring of it,against verify runs only. Fingerprint matching stays case-sensitive.

Hits are ordered by match strength (text outweighs a command, which outweighsa digest; an exact fingerprint outweighs a substring), then by recency. Withno keywords and no filters, that is simply the newest retained rows. Eachline is labelled trusted or agent. Agent hits stay unverified. limitclamps to 0–20 and defaults to 5. The command is read-only and does not create.agent-guard/ when the store is absent. Rows rotated past the third segmentare already gone and are not searched.

Evidence and loop detection

guard_verify runs your command with the caller's privileges, spools output totemporary files to bound RAM, and records one append-only entry: ID, exit code,duration, command, and a digest of the output. Stdin is closed, so a commandcannot read the MCP protocol stream or hang waiting for input. IDs areallocated inside the shared lease, so parallel pi and CLI processes nevercollide. If the ledger's last line was torn by a crash, the next append startson its own line instead of gluing two JSON values together.

The report is the part an agent should read. A failure includes the tail ofthe output, why that exit happened, and a concrete fix. Exit 0 is recordedhonestly, but a pipeline (no pipefail) or a failure mask (|| true, ; :)is labelled UNTRUSTED and must not be cited as proof. The CLI exits 1 forthat case. ANSI color is stripped before the 2000-character digest cap so thetail stays readable.

Loop detection still stores the frozen fingerprint (whitespace collapsed,nothing else). The streak key also ignores a trailing ; outside quotes and atrailing failure mask, so pytest -q; and npm test || true count as thesame command as pytest -q and npm test. An unproven exit 0 — a failuremask, or a pipeline that never checked its earlier stages — does not reset thestreak; it counts as one more attempt, so masking repeatedly cannot walk acommand back under the threshold. At loop_block_after the attempt isrefused, with the prior failure count and a change-strategy nudge.

Verification workflow: run a command, persist its evidence, and cite the result. With the default threshold, two failures of the same normalized command block its third attempt; only that command succeeding resets its streak

Scope audit

Diffs are computed with git porcelain/numstat over tracked, staged, andbounded non-ignored untracked files, then compared against the configured lineand file thresholds to produce ok, warn, or hard. The pi extension freezesits baseline at session start, so committing mid-session does not reset thescope; CLI and MCP default to HEAD and accept an explicit baseline.

Untracked content reads are capped at 1,000 files, 1 MB per file, and 8 MBtotal, and stop once the hard line threshold is known. A truncated scan isreported as a bounded scan, promoted to at least a warning (CLI exit 1), andits count treated as a lower bound — the widget renders it as Δ≥.

Precedents and satisfaction

Recording is not the same as remembering. Two conventions make a journal entryreusable instead of merely stored:

  • A lesson needs trigger words you would actually search: guard_note saysso when a lesson carries no tag outside the generic set, because a lessonwhose only tag is lesson is reachable by nothing but its own wording.
  • Tag an entry lesson and it becomes a precedent: the brief re-injects itunder precedents:, ahead of the recent decisions: andagent notes (unverified): caps, so a lesson the owner already paid forcannot be pushed out by newer session chatter. Precedents are ranked by howmuch of the current task they answer, then by recency, and the cap isbrief_max_precedents in config.json (default 3, 0 disables the section).Keep each to one actionable sentence — "when a review lists N follow-ups,take all N" — because it is re-sent every turn.
  • Tag a correction satisfied or dissatisfied to record how the ownerreacted to a decision, plus the action that produced that reaction. The nextsimilar situation then starts from the outcome instead of re-litigating it.These two words are a fixed vocabulary: a near-miss such as dissatisfed isnormalized on write and reported in the acknowledgement, so owner feedbackcannot silently decay into an ordinary tag.

Recall is paraphrase-tolerant rather than exact-string: common stopwords aredropped, tags are searched as synonyms, and hits are weighted by rarity, soa word that appears in every entry cannot outvote the one word that answers thequestion. "why did the owner get annoyed?" therefore reaches a lesson taggedannoyed even though the text says "prefers the broad fix" — the tag is theowner's own word, written once and reused forever. One tag match also satisfiesthe multi-word floor on its own; otherwise a three-or-more-word query needs tworeal matches so a single shared word cannot answer a long question.

Matching is word-boundary aware: old no longer hits threshold or search,while a prefix still finds its own inflections (patch → patches).

When a query is given, past successful bash commands from events.jsonl jointhe hits as command · re-run to verify · ran N× · last ok exit 0 rows at halfweight — a repeated chore ("copy these to that folder") starts from the commandthat already worked instead of being re-derived, and the row carries theoutcome the command last had, because a command that ran is not a command thatstill works. A family whose last observed run failed is demoted and labelledlast FAILED — change it before reuse; refused or declined calls are neverreused. Non-shell tools leave their own trail: a read/edit/search callrecords the path or pattern it touched, so the same query also returnsread · reuse · used 2× · <path> hints — "you were here before" instead ofre-deriving where the code lives. Those keys are paths, never commands, and onlybash and guard_verify rows may form a loop streak, so a hint can never beblocked as a repeated failure.

Trust and provenance

Direct CLI and pi slash-command contract writes are treated as owner input.MCP/model journal writes are forcibly tagged agent: they never enter thetrusted decisions:/corrections: sections. They are not discarded either —the brief shows the newest two under corrections (unverified): when theyare corrections, else agent notes (unverified):, clearly labelled and cappedshorter than trusted text, and guard_journal returns thefull stored entry. That keeps the model's own reasoning recoverable after acompaction without granting it authority. CLI and pi slash-command journalentries are trusted. Untaggedlegacy entries are treated as trusted for backward compatibility, because thefrozen journal schema has no dedicated author field.

This is a provenance convention, not authentication. A model that already hasshell access can invoke the CLI and appear on that trusted path; use host-leveltool permissions when the distinction must be a security boundary.

All newly stored contract, journal, and tag text is normalized to one boundedline, and legacy values are normalized again when rendered. This preventsterminal-line forging and unbounded persistent prompt growth; it does not makeuntrusted prose safe to follow.

Context budget

Every injected and printed block has a cap: brief sections, journal and contracttext, evidence digests, orientation output, and previews. Context-watermarknotifications fire at ctx_warn_pct and above, and the widget always shows thecurrent percentage. The result is a guardrail layer whose own cost staysconstant while a session grows.

Development and validation

.venv/bin/pytest -q                          # Python suites
npm run typecheck                            # extension types (tsc --noEmit)
npm test                                     # extension unit tests (node --test)
.venv/bin/python -m compileall -q src        # byte-compile check
.venv/bin/python -m build --outdir /tmp/protect-context-dist

npm run check runs the typecheck and the extension tests together. Thedefinition-of-done checks are run twice consecutively before release. The datedaudit records the observed counts, integration probes, and package smoke testinstead of leaving stale numbers in this README.

Path Contents
src/protect_context/ Store, contract, journal, recall, verify, loops, diff scope, orientation, preflight, budget, report, CLI, MCP server
extension/ pi entry point (index.ts) and pi-import-free lib/ modules
tests/ pytest suites, including a fastmcp in-memory client smoke over every MCP tool
extension/test/ node --test suites for fingerprinting, risk patterns, loops, brief digest, widget, git parsing, config
SPEC.md Frozen v1.0 contract — structure, defaults, and definition of done

Troubleshooting

Symptom Likely cause Fix
No brief appears in pi Digest unchanged (brief_mode: "changed") and nothing new to say Expected on quiet turns; set brief_mode: "always" to see it every turn
Δ? in the widget Not a Git worktree, so no diff baseline Run inside a repository, or use the CLI with an explicit --baseline
Δ≥ in the widget Bounded untracked scan hit a cap Treat the count as a lower bound; narrow the scan or review scope manually
Loop never blocks Commands differ by flags or paths. A trailing semicolon or an or-true failure mask shares a streak; other edits do not Compare with loops / guard_loop_check. Change the command for real — do not mask the failure
Verify says UNTRUSTED, or CLI verify exits 1 after exit 0 The shell status is only the last stage of a pipeline, or the command forces success with true Re-run without the mask. For a pipeline the tool returns the exact rewrite to paste (bash -o pipefail -c '<command>'). Do not cite that evidence id
Verify timed out, or an MCP session stalled on an old build The command waited on stdin, or it exceeded timeout_s (clamped to 1–900, recorded as exit 124) Current builds close stdin. Fix the hang or pass a larger timeout_s
Agent forgot its own earlier decision after a compaction The brief shows only the newest 2 agent notes (plus 2 unverified corrections), 120 chars each Read the full entry with guard_journal, or search tags and evidence with guard_recall
No guard block after an automatic compaction Host emits no context event, so mid-run re-assertion cannot run It returns at the next user prompt; check the host supports the context hook
task: line is wrong or missing Auto-capture ignores short prompts and slash commands, and an owner task always wins Set it explicitly with /guard-task <text> or /guard-task --clear
PC_SESSION_ID changes nothing in evidence It scopes ledger views, not state.json Expected: state is project-global by design
MCP tool missing in host Host is not launching the PyPI server, or a checkout server is not using its venv interpreter Use command uvx and args ["protect-context", "mcp"]. From a checkout, point command at .venv/bin/python and pass -m protect_context.mcp_server
Extension does not load Node older than 22.19 or pi not restarted Check pi list, then restart; use pi -ne to start without extensions
Contract edit rejected over MCP Removing a frozen seed constraint Seeds are protected; use the CLI or a pi slash command as the owner
Published, but pi.dev/packages says "No packages match" The gallery index is built from npm search, and npm drops search documents whose downloads.monthly is 0. A publish rewrites that document with the download figure of that moment, so publishing before npm's first download rollup pins it at 0 Run scripts/check-discoverability.sh; if the document's downloads are 0 while the downloads API reports traffic, publish once the rollup exists (npm re-crawls documents on its own as well, typically within a week)

Limitations and security boundary

Shell execution
  • guard_verify deliberately invokes a shell with the caller's privileges. Itcan modify files, access the network, or disclose data. The pi layer asks forconfirmation only when one of the frozen regex patterns matches; the MCP layerrelies on host approval. Neither is a shell parser or a sandbox.
  • MCP annotations are untrusted hints. A client may ignore them. Do not exposeguard_verify to an untrusted model without a real host sandbox/approvalpolicy.
Evidence
  • An evidence record proves only that one command returned one exit code. It isnot cryptographically bound to a commit/worktree, has no freshness policy, anddoes not prove the claimed behavior. Exit 0 from a pipeline or a failure maskis labelled UNTRUSTED in the report for that reason. The stored ok fieldstays the shell's real status.
Scope
  • The pi extension freezes its diff baseline at session start, so committingmid-session does not reset its scope. CLI/MCP audits default to currentHEAD; pass them an explicit baseline for cross-commit scope. Binary filescount as files but zero lines.
  • Untracked-file content reads are capped at 1,000 files, 1 MB per file, and8 MB total, and stop once the hard line threshold is known. Output explicitlyreports a bounded scan, promotes it to at least a warning/CLI exit 1, andtreats the truncated count as a lower bound.
Determinism
  • The frozen brief carries at most 12 constraints, 3 trusted decisions, 3trusted corrections, 2 unverified corrections, and 2 agent notes, plus atmost 2 trusted tag matcheswhen the owner task names a journal tag the caps hid. Overflow is visible(each section announces what it hid) and points at guard_journal andguard_recall, but hidden entries are not enforced by context injection.
  • Frozen fingerprint normalization collapses whitespace everywhere, includinginside quoted shell strings. Semantically different commands can thereforeshare a loop fingerprint.
  • The deterministic contract checker is keyword coverage, not program analysis.Treat every addressed result as a review prompt, not proof.
Compaction
  • Re-injection is only as good as the store. A constraint, decision, or taskthat was never written down cannot be restored, and the brief is a boundedpointer rather than a transcript; guard_journal returns the full entry.
  • A mid-run compaction (threshold or overflow recovery) resumes the turnthrough AgentStart, where before_agent_start does not run. The extensionarms a context hook on session_compact and re-asserts the block on everyrequest until a persisted one is back in the conversation. On a host thatdoes not emit context, the block returns at the next user prompt instead.
  • Task capture stores the first substantive prompt (≤400 chars) in.agent-guard/state.json. That is conversation content: it stays local andgitignored, but review it before sharing a store, and clear it with/guard-task --clear.
  • Agent notes are unverified context, never authority. A model that journals"ignore the contract" is shown that sentence back under an explicitlyunverified heading; treat it as data, and keep owner constraints in thecontract where they are trusted.
Store
  • state.json is project-global. Atomic replacement prevents torn JSON, butsimultaneous sessions can still overwrite notification/session state; usePC_SESSION_ID to separate ledger views, not state files.
  • Commands and the last 2,000 output characters are stored verbatim. There isno automatic secret detector or redactor; never put credentials in commands orverification output.
  • JSONL ledgers rotate once the live file passes 1 MiB, keeping three oldersegments. Recall and the other readers see that retained window only; rowsrotated past the third segment are gone. Output views stay bounded.
  • Python verification bounds RAM by spooling output to temporary files, but avery noisy command can consume temporary disk until it exits. On Windows,killing the direct process cannot guarantee that every descendant exits.

Design sources

The implementation was checked against primary or professional sources:

Non-goals

No network or LLM calls are made by the guardrail logic itself. It is not asandbox, policy engine, auto-fixer, secret manager, or replacement for pi/MCPhost permissions. Recall is a local scan of the retained JSONL window: thesuite does not embed journal or evidence text and does not add a vectordatabase. The suite complements host permissions with durable context,evidence, telemetry, and narrowly scoped blocking.

If this saved you a re-run, a star helps other people find it.

Star history

License

MIT — see LICENSE. Copyright (c) 2026 XJTLUmedia.

MCP Server · Populars

MCP Server · New

    OyadotAI

    Oya Browser

    The browser control plane for AI agents. One API over Oya Cloud, Browserbase, Steel, Anchor, Browser Use and your own Chrome, with persistent personas, CAPTCHA and MFA handling, and live human takeover.

    Community OyadotAI
    unibaseio

    Membase

    Memory layer for AI agents — SDKs, CLI and MCP server for Membase

    Community unibaseio
    EternalNight996

    🧠 memory-eternal — a second brain for your AI

    Your AI forgets every session. Give it a memory you can audit: local-first, review-gated agent memory — one SQLite vault for DSH, Claude Code, Codex, Cursor, OpenClaw, Hermes and MengBot (any MCP client), with an immutable audit log and a token ledger. Agents propose, humans approve. MIT, self-hosted, no cloud.

    Community EternalNight996
    quarkiverse

    Model Context Protocol Servers in Quarkus/Java

    Model Context Protocol Servers in Quarkus

    Community quarkiverse
    magiksoft

    AiderDesk

    Platform for AI-powered software engineers

    Community magiksoft