--- issue: 413 issue_title: "Explicitly allow some external directories relative to the home directory" --- # Retro: #413 — Explicitly allow some external directories relative to the home directory ## Stage: Planning (2026-06-16T15:06:55Z) ### Session summary Investigated a third-party request (filed by `michaelmior`) to allow outside-CWD directories like `~/.cargo/registry` without prompting. Found the capability already exists via the `external_directory` surface pattern map; the user's `path`-surface attempt failed because of most-restrictive-wins composition and a missing trailing `*`. After confirming direction with the operator, wrote a docs-only plan to make the `external_directory` allow-list discoverable across `configuration.md`, `README.md`, `config.example.json`, and the schema. ### Observations - This is fundamentally a discoverability bug, not a missing feature. `"external_directory": { "*": "ask", "~/.cargo/registry/*": "allow" }` already does what the user wants. - Rejected the tempting "make a `path` allow suppress the `external_directory` gate" approach. The four layers compose with most-restrictive-wins, so a `path` allow loosening an `external_directory: ask` boundary would be a security regression, not a fix. The operator's instinct (keep `external_directory` as a separate, intentional layer modeled on OpenCode) confirmed this. - Operator correction on wildcard semantics: do **not** use `**` in examples. A single `*` compiles to a greedy `.*` (with the `s` flag in `wildcard-matcher.ts`) and already crosses subdirectory boundaries, so `~/.cargo/registry/*` matches every file beneath the directory. - Scope trimmed by operator: `configuration.md` + `README.md` + `config.example.json`/schema; `troubleshooting.md` deselected. - Next stage is `/build-plan` (docs-only), not `/tdd-plan`. - Alignment constraint applies: `configuration.md`, `README.md`, `config.example.json`, and `schemas/permissions.schema.json` must agree on the `external_directory` pattern-map form. ## Stage: Implementation — Build (2026-06-16T15:30:00Z) ### Session summary Executed all three docs-only build steps from the plan, one commit each: `configuration.md` (cache-dir recipe, "which surface?" clarification, single-`*` note, most-restrictive-wins reaffirmation), `README.md` (clear `external_directory` surface description + pattern-map example), and `config/config.example.json` + `schemas/permissions.schema.json` (inline `~/.cargo/registry/*` allow example, aligned across both). No `src/` or `test/` files were touched, so the test/typecheck suites were not required; the package lint passes and both JSON files parse. ### Observations - The schema `markdownDescription` is a single physical JSON line with literal `\n` escape sequences, not real newlines. An `Edit` `oldText` built with actual newlines failed to match; matching the literal `\n\n` (and escaping the embedded JSON quotes as `\\\"`) was required. - Operator mid-build question confirmed `jsonc` is the correct doc fence language: the docs use ` ```jsonc ` 21 times and ` ```json ` zero times, and the loader runs `JSON.parse(stripJsonComments(raw))` (`config-loader.ts:388`), so `//` comments are supported (trailing commas are not). The config file itself stays `.json` and comment-free. - No deviations from the plan. The optional `piInfrastructureReadPaths` cross-reference (Open Question) was included as a single sentence in `configuration.md` only, as the plan leaned. - Pre-completion reviewer: PASS — ready for `/ship-issue`. No WARN findings. ## Stage: Final Retrospective (2026-06-16T16:00:00Z) ### Session summary Shipped a docs-only fix for a third-party discoverability issue: the `external_directory` allow-list (e.g. `~/.cargo/registry/*`) already does what the user wanted, so the work documented it across `configuration.md`, `README.md`, `config.example.json`, and the schema rather than changing behavior. The session ran clean end to end (plan → build → ship → release `pi-permission-system` `v13.1.2`), with the only notable inflection at planning: an initial mis-framing of the design that the operator's redirect and a most-restrictive-wins re-analysis corrected before any code was written. ### Observations #### What went well - The `ask_user` gate on a third-party issue earned its keep. It surfaced the design direction before implementation and prevented building the tempting-but-wrong "make a `path` allow suppress `external_directory`" change, which most-restrictive-wins makes a silent access-widening regression. - Incremental verification throughout build: `lint` ran after each of the three doc steps, and both JSON files were `JSON.parse`-checked after the schema/example edit — no end-only verification gap. - Operator mid-build spot-check ("`jsonc` or `json`?") was answered by verifying the convention (21 ` ```jsonc ` vs 0 ` ```json `) and the loader (`JSON.parse(stripJsonComments(raw))`), not by assertion — and the answer held. #### What caused friction (agent side) - `missing-context` — the first planning `ask_user` presented "make a `path` allow authoritative (suppress `external_directory`)" as a near-recommended fix, framed as correcting an "asymmetry," without first tracing the documented most-restrictive-wins composition where `ask` > `allow`. The operator redirected to discuss the interaction; a re-analysis then showed the option was a security regression. Self-corrected after the operator's nudge. Impact: one extra discussion round at planning; no rework (caught before any code), and the outcome improved. - `other` (tool-usage) — two consecutive `Edit` calls on `configuration.md`/`schemas/permissions.schema.json` were rejected: first for a stray `newText_unused`/`newText2_unused` key in the `edits` object, then because the `oldText` for the schema `markdownDescription` used real newlines while the file stores literal `\n` escapes inside a single-line JSON string. Impact: ~2 retries, no rework. #### What caused friction (user side) - None blocking. The operator's two interventions (planning redirect, `jsonc` spot-check) were timely and strategic, and both improved the result. The only forward-looking opportunity: the most-restrictive-wins composition invariant lives in `docs/configuration.md` but not in the package skill, so it was not front-of-mind when I drafted the first `ask_user` — see the proposed skill addition. ### Diagnostic details - **Model-performance correlation** — one subagent dispatch (`pre-completion-reviewer`) ran on `anthropic/claude-sonnet-4-6` for judgment-heavy doc/consistency review; appropriate, no mismatch. - **Escalation-delay tracking** — no `rabbit-hole`; the longest stuck sequence was the two rejected `Edit` calls, resolved on the third attempt (under the 5-call threshold). - **Unused-tool detection** — none; `grep`/`Read` covered the planning context, and no `missing-context` point would have been closed by an unused agent or tool (the gap was an undocumented invariant, not unsearched code). - **Feedback-loop gap analysis** — `lint` ran after every build step and JSON parsing after the schema edit; no deferred-verification gap. `check`/`test` were correctly skipped (no `src/`/`test/` changes). ### Changes made 1. `.pi/skills/package-pi-permission-system/SKILL.md` — added a most-restrictive-wins cross-surface composition invariant after the `last-match-wins` bullet (Implementation Priorities), stating that a more-permissive rule on one surface cannot loosen a more-restrictive rule on another and that outside-CWD directories belong on `external_directory`, not `path`. 2. Proposal 2 (JSON-escape `Edit` note in `AGENTS.md`) was considered and declined by the operator — not landed.