9.3 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 481 | pi-permission-system: env-var prefix and bash -c/eval bypass bash command-pattern rules |
Retro: #481 — pi-permission-system: env-var prefix and bash -c/eval bypass bash command-pattern rules
Stage: Planning (2026-06-26T00:00:00Z)
Session summary
Produced a numbered TDD plan for two bash command-pattern bypasses reported by a third-party author (0xbentang): a leading variable_assignment env-var prefix defeating rule matching, and opaque -c/eval payloads riding a permissive allow.
Confirmed direction and scope with the operator via ask_user (the issue is third-party, so the direction gate was required).
Filed follow-up #490 for other indirection wrappers and committed the plan at packages/pi-permission-system/docs/plans/0481-strip-env-prefix-floor-opaque-bash-wrappers.md.
Observations
- Both bypasses are fixed in this plan, but via two different mechanisms decided with the operator:
- Part 1 strips the env-var prefix from the emitted command unit (re-targets matching at the real command).
- Part 2 floors opaque shell wrappers (
bash/sh/dash/zsh/ksh -c, pluseval) to at leastaskrather than re-parsing the payload.
- The operator explicitly preferred the floor-to-
askapproach over the issue's "ideally re-parse-c/eval" suggestion — it is fail-safe, far simpler, and avoids the path-candidate asymmetry that re-parsing would leave (inner paths would still miss thepath/external_directorysurfaces). - Floor semantics:
allowclamps up toaskwith an<opaque-bash-wrapper>sentinel; an explicitdenyrule still wins. Mirrors the existing<unparseable-bash-command>sentinel (#452). - Classified
fix:notfix!:— it only tightens decisions (closes a bypass), never weakens one; the old behavior was the bug, so there is no intended behavior to preserve. - Wrapper set scoped to inline shells +
eval; other indirection wrappers (sudo,env VAR=x cmd,xargs,find -exec,time,nohup,timeout,nice) deferred to #490. - Implementation note for the next stage: Part 1 needs
startIndexadded to the minimalTSNodeinterface (parser.ts) to slice verbatim text; this makes it a required field, so themakeNodeliteral builder intest/access-intent/bash/node-text.test.tsmust setstartIndex: 0in the same commit (typecheck coupling). - The metamorphic totality test (
bash-command-metamorphic.test.ts) wraps with acdprefix, notbash -c, so the opaque floor does not disturb it. #481is not in the architecture roadmap (Phase 6 is complete) → ship independently.
Stage: Implementation — TDD (2026-06-26T22:30:00Z)
Session summary
Implemented all three planned TDD steps across two fix: commits and supporting docs: stripping the leading variable_assignment prefix from each bash command unit, and flooring opaque -c/eval wrappers (bash/sh/dash/zsh/ksh -c + eval) from allow to ask with a <opaque-bash-wrapper> sentinel.
Test count went from 2124 → 2145 (+21) in pi-permission-system; full suite, pnpm run check, root lint, and pnpm fallow dead-code all green.
Pre-completion reviewer returned PASS.
Observations
- Established the green baseline by fixing two pre-existing lint failures first (a separate
docs:commit): the0481plan wrapped its[#NNN]reference links in backticks (MD053 unused-definition), and the archivedphase-6history doc pointed at a#directory-organization-forward-lookinganchor that was consolidated into## Module structure(MD051). - Step 1 coupling held as the plan predicted: adding the required
startIndexfield toTSNodeforced themakeNodeliteral builder innode-text.test.tsto setstartIndex: 0in the same commit. - The opaque detector (
isOpaqueWrapperCommand) matches thecommand_namebasename against a shell set (bash/sh/dash/zsh/ksh) pluseval, and recognizes-cinside a short-flag cluster (-c/-ec/-xc);grep -cis unaffected becausegrepis not a shell. The detector skips the same leadingvariable_assignmentprefix ascommandUnitText, so an env-prefixedAWS_PROFILE=x bash -c "…"is both stripped and flagged. - The
eslintpre-commit hook auto-fixed one optional-chain in step 2 (!child || !child.isNamed→!child?.isNamed);biomeseparately flagged a second optional-chain incommandUnitTextthat eslint left, landed as astyle:commit. - Minor plan deviation: the plan listed a
program.tscommands()JSDoc update; I initially updated only thecollectCommandsJSDoc, then added thecommands()JSDoc in a follow-updocs:commit. - Pre-completion reviewer: PASS.
Reviewer warnings (addressed in a follow-up
docs:commit, not deferred): the README "Fails closed" bullet and thepackage-pi-permission-systemskill Debugging section named only<unparseable-bash-command>and now also mention the sibling<opaque-bash-wrapper>sentinel.
Stage: Final Retrospective (2026-06-27T03:00:00Z)
Session summary
Shipped #481 end-to-end across planning, TDD, and ship stages: two fix: commits closing the env-var-prefix and opaque-wrapper bash-gate bypasses, released as pi-permission-system-v16.2.1.
The pipeline ran cleanly — third-party direction gate honored in planning, pre-completion reviewer PASS, CI green, release-please PR merged by rebase.
The only rework was a baseline-cleanup commit at the start of TDD to fix markdown-lint failures the planning stage introduced into its own plan file.
Observations
What went well
- The two-
ask_userplanning gate for a third-party issue worked as intended: the first call settled scope + commit classification, and when the operator answered a scoping question with a counter-question ("don't we punt opaque payloads to ask?"), a secondask_userconverged on the floor-to-askdesign and wrapper set before any code was planned. This avoided building the heavier re-parse approach the issue author suggested. - Incremental verification held throughout TDD:
pnpm run checkran immediately after the step-1TSNodeinterface change (a shared-type edit), the full suite ran after every step, andpnpm run lint+pnpm fallow dead-coderan before the ship push — no end-of-session surprise. - The ship prompt's step-6.4 guidance paid off: release PR #491 had a CI
checkstillIN_PROGRESS, and the flow correctly waited for it to finish rather than falling back togh pr mergewhile a check was running, then merged by rebase.
What caused friction (agent side)
instruction-violation(self-identified) — the planning stage wrote issue cross-references as backticked`[#452]`/`[#306]`/`[#393]`in the plan body, which renders them as code spans, not link references; the[#N]:definitions at the file foot then had no matching reference and tripped MD053. This surfaced at the TDD green-baseline lint check and forced a separatedocs:cleanup commit (5915260d) before TDD could start. Impact: one extra commit and a baseline detour; no wrong code. Themarkdown-conventionsskill already states "every[#N]:definition must have a matching[#N]reference," but does not warn that backticks disqualify the reference — the exact failure mode here.scope-drift(minor) — the plan listed aprogram.tscommands()JSDoc update that the first TDD pass missed (only thecollectCommandsJSDoc was updated); caught during the Module-Level-Changes cross-check and fixed in a follow-updocs:commit (18920980). Impact: one extra small commit; no rework.other(tooling) — theeslintpre-commit hook auto-fixed one optional-chain in step 2, butbiomeindependently flagged a second optional-chain incommandUnitTextthat only surfaced at the end-of-step root lint, landing as a separatestyle:commit (8cef1c88). Impact: one extra commit; the two linters do not agree on which optional-chains they auto-fix at commit time.
What caused friction (user side)
- None. The operator's counter-question during planning was a net positive — it redirected toward the simpler, fail-safe design before code was written, exactly the kind of early strategic intervention the workflow wants.
Diagnostic details
- Model-performance correlation — the single subagent dispatch (
pre-completion-reviewer) ran onanthropic/claude-sonnet-4-6, appropriate for the judgment-heavy review (acceptance criteria, design, cross-step invariants); no mismatch. - Escalation-delay tracking — no
rabbit-holefriction; no error or approach occupied more than two consecutive tool calls. - Unused-tool detection — none warranted; exact-symbol exploration (
grep/Read) was the right fit for tracingcollectCommandsInto/makeUnit/variable_assignment, and the one judgment task was correctly delegated to the reviewer subagent. - Feedback-loop gap analysis — no gap; verification was incremental (typecheck after the interface change, full suite per step, lint + fallow before push) rather than end-loaded.
Changes made
.pi/skills/markdown-conventions/SKILL.md— added a rule to the "Issue references" subsection: a[#N]wrapped in backticks is a code span, not a link reference, so the[#N]:definition still trips MD053; write[#N]as plain text, including inside other formatting.