Skip to content

PHAX: Scoped approval hook for declared Claude-Code protected-path edits - #62

Open
lbdremy wants to merge 5 commits into
mainfrom
phax/protected-path-approval--phase-05
Open

PHAX: Scoped approval hook for declared Claude-Code protected-path edits#62
lbdremy wants to merge 5 commits into
mainfrom
phax/protected-path-approval--phase-05

Conversation

@lbdremy

@lbdremy lbdremy commented Jun 30, 2026

Copy link
Copy Markdown
Owner

PHAX Run Review Handoff

Generated by PHAX.

Run Review Handoff

Run summary

  • Short Name: protected-path-approval
  • Run ID: protected-path-approval-1782818550964
  • Base Branch: phax/protected-path-approval
  • Final Phase Branch: phax/protected-path-approval--phase-05
  • Gate Profile: full
  • Phases: 5/5 passed
  • See final-report.md for security details and entry/resume instructions.

Global File Reconciliation

Run: phax.protected-path-approval

File Planned in Touched in Status Notes
.agents/skills/phax-planning/SKILL.md phase-05 missing not touched in: phase-05
docs/security/protected-path-edits.md phase-05 phase-05 matched
phax.schema.json phase-02 phase-02 matched
scripts/generate-usage-spec.ts phase-04 unplanned unplanned in: phase-04
src/app/executePlan.ts phase-03 phase-03 matched
src/cli/commands/approveProtectedPath.ts phase-04 phase-04 matched
src/cli/introspect.ts phase-04 unplanned unplanned in: phase-04
src/cli/main.ts phase-04 missing not touched in: phase-04
src/cli/program.ts phase-04 unplanned unplanned in: phase-04
src/domain/security/protectedPaths.ts phase-01 phase-01 matched
src/domain/security/resolvePolicy.ts phase-02 phase-02 matched
src/domain/security/resolveReviewPolicy.ts phase-02 unplanned unplanned in: phase-02
src/domain/security/types.ts phase-02 phase-02 matched
src/infra/providers/claudeCode.ts phase-04 phase-04 matched
src/infra/providers/protectedPathHookSettings.ts phase-04 phase-04 matched
src/ports/backend.ts phase-03 phase-03 matched
src/schemas/claudeHookPayload.ts phase-04 phase-04 matched
src/schemas/securityConfig.ts phase-02 phase-02 matched
src/schemas/securityPosture.ts phase-03 unplanned unplanned in: phase-03
tests/e2e/gateExhaustionResume.test.ts phase-03 unplanned unplanned in: phase-03
tests/e2e/resetPhase.test.ts phase-03 unplanned unplanned in: phase-03
tests/e2e/semanticTrace.providers.test.ts phase-03 unplanned unplanned in: phase-03
tests/e2e/semanticTrace.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/adjustPlanCommand.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/claudeProtectedPathHook.test.ts phase-04 phase-04 matched
tests/integration/cliProgram.test.ts phase-04 unplanned unplanned in: phase-04
tests/integration/enter.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/enterPhase.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/executePlan.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/perPhaseBranch.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/plansOverlapCommand.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/plansOverlapLanded.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/protectedPathApprovals.test.ts phase-03 phase-03 matched
tests/integration/providerDispatcher.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/rateLimit.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/reconciliation.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/resume.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/resumeFromCleanup.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/resumeFromCommit.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/resumeHandoff.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/reviewCodeCommand.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/reviewCompliance.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/reviewComplianceCommand.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/routing.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/runFolder.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/sessionInfo.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/setupFailure.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/stateMachineContract.test.ts phase-03 unplanned unplanned in: phase-03
tests/integration/telemetry/end-to-end.test.ts phase-03 unplanned unplanned in: phase-03
tests/unit/dryRun.test.ts phase-03 unplanned unplanned in: phase-03
tests/unit/gateProfile.test.ts phase-03 unplanned unplanned in: phase-03
tests/unit/protectedPaths.test.ts phase-01 phase-01 matched
tests/unit/providers/claudeCode.test.ts phase-03 unplanned unplanned in: phase-03
tests/unit/providers/codexCli.test.ts phase-03 unplanned unplanned in: phase-03
tests/unit/providers/mistralVibe.test.ts phase-03 unplanned unplanned in: phase-03
tests/unit/resolvePolicyProtected.test.ts phase-02 phase-02 matched
tests/unit/security/capabilities.test.ts phase-03 unplanned unplanned in: phase-03
tests/unit/security/posture.test.ts phase-03 unplanned unplanned in: phase-03
tests/unit/security/resolvePolicy.test.ts phase-02 unplanned unplanned in: phase-02
tests/unit/security/resolveReviewPolicy.test.ts phase-03 unplanned unplanned in: phase-03

Global unplanned changes

  • scripts/generate-usage-spec.ts (touched in: phase-04)
  • src/cli/introspect.ts (touched in: phase-04)
  • src/cli/program.ts (touched in: phase-04)
  • src/domain/security/resolveReviewPolicy.ts (touched in: phase-02)
  • src/schemas/securityPosture.ts (touched in: phase-03)
  • tests/e2e/gateExhaustionResume.test.ts (touched in: phase-03)
  • tests/e2e/resetPhase.test.ts (touched in: phase-03)
  • tests/e2e/semanticTrace.providers.test.ts (touched in: phase-03)
  • tests/e2e/semanticTrace.test.ts (touched in: phase-03)
  • tests/integration/adjustPlanCommand.test.ts (touched in: phase-03)
  • tests/integration/cliProgram.test.ts (touched in: phase-04)
  • tests/integration/enter.test.ts (touched in: phase-03)
  • tests/integration/enterPhase.test.ts (touched in: phase-03)
  • tests/integration/executePlan.test.ts (touched in: phase-03)
  • tests/integration/perPhaseBranch.test.ts (touched in: phase-03)
  • tests/integration/plansOverlapCommand.test.ts (touched in: phase-03)
  • tests/integration/plansOverlapLanded.test.ts (touched in: phase-03)
  • tests/integration/providerDispatcher.test.ts (touched in: phase-03)
  • tests/integration/rateLimit.test.ts (touched in: phase-03)
  • tests/integration/reconciliation.test.ts (touched in: phase-03)
  • tests/integration/resume.test.ts (touched in: phase-03)
  • tests/integration/resumeFromCleanup.test.ts (touched in: phase-03)
  • tests/integration/resumeFromCommit.test.ts (touched in: phase-03)
  • tests/integration/resumeHandoff.test.ts (touched in: phase-03)
  • tests/integration/reviewCodeCommand.test.ts (touched in: phase-03)
  • tests/integration/reviewCompliance.test.ts (touched in: phase-03)
  • tests/integration/reviewComplianceCommand.test.ts (touched in: phase-03)
  • tests/integration/routing.test.ts (touched in: phase-03)
  • tests/integration/runFolder.test.ts (touched in: phase-03)
  • tests/integration/sessionInfo.test.ts (touched in: phase-03)
  • tests/integration/setupFailure.test.ts (touched in: phase-03)
  • tests/integration/stateMachineContract.test.ts (touched in: phase-03)
  • tests/integration/telemetry/end-to-end.test.ts (touched in: phase-03)
  • tests/unit/dryRun.test.ts (touched in: phase-03)
  • tests/unit/gateProfile.test.ts (touched in: phase-03)
  • tests/unit/providers/claudeCode.test.ts (touched in: phase-03)
  • tests/unit/providers/codexCli.test.ts (touched in: phase-03)
  • tests/unit/providers/mistralVibe.test.ts (touched in: phase-03)
  • tests/unit/security/capabilities.test.ts (touched in: phase-03)
  • tests/unit/security/posture.test.ts (touched in: phase-03)
  • tests/unit/security/resolvePolicy.test.ts (touched in: phase-02)
  • tests/unit/security/resolveReviewPolicy.test.ts (touched in: phase-03)

Global missing planned changes

  • .agents/skills/phax-planning/SKILL.md (planned in: phase-05)
  • src/cli/main.ts (planned in: phase-04)

Global review attention points

Deviations not explained in any handoff

  • scripts/generate-usage-spec.ts
  • src/cli/introspect.ts
  • src/cli/program.ts
  • tests/e2e/gateExhaustionResume.test.ts
  • tests/e2e/resetPhase.test.ts
  • tests/e2e/semanticTrace.providers.test.ts
  • tests/e2e/semanticTrace.test.ts
  • tests/integration/adjustPlanCommand.test.ts
  • tests/integration/cliProgram.test.ts
  • tests/integration/enter.test.ts
  • tests/integration/enterPhase.test.ts
  • tests/integration/executePlan.test.ts
  • tests/integration/perPhaseBranch.test.ts
  • tests/integration/plansOverlapCommand.test.ts
  • tests/integration/plansOverlapLanded.test.ts
  • tests/integration/providerDispatcher.test.ts
  • tests/integration/rateLimit.test.ts
  • tests/integration/reconciliation.test.ts
  • tests/integration/resume.test.ts
  • tests/integration/resumeFromCleanup.test.ts
  • tests/integration/resumeFromCommit.test.ts
  • tests/integration/resumeHandoff.test.ts
  • tests/integration/reviewCodeCommand.test.ts
  • tests/integration/reviewCompliance.test.ts
  • tests/integration/reviewComplianceCommand.test.ts
  • tests/integration/routing.test.ts
  • tests/integration/runFolder.test.ts
  • tests/integration/sessionInfo.test.ts
  • tests/integration/setupFailure.test.ts
  • tests/integration/stateMachineContract.test.ts
  • tests/integration/telemetry/end-to-end.test.ts
  • tests/unit/dryRun.test.ts
  • tests/unit/gateProfile.test.ts
  • tests/unit/providers/claudeCode.test.ts
  • tests/unit/providers/codexCli.test.ts
  • tests/unit/providers/mistralVibe.test.ts
  • tests/unit/security/capabilities.test.ts
  • tests/unit/security/posture.test.ts
  • tests/unit/security/resolveReviewPolicy.test.ts
  • src/cli/main.ts

Plan compliance review

Plan-compliance review — phax.protected-path-approval

Run-level verdict: conformant-with-deviations

All five phases delivered their objectives. Deviations fall into three categories: (1) cascade updates forced by TypeScript's structural type system when required fields were added to shared interfaces, (2) CLI file routing (subcommand registered in program.ts instead of planned main.ts), and (3) one uncommitted working-tree edit in the final docs phase. Categories 1 and 3 are fully justified and documented; category 2 is structurally equivalent but lacks a handoff explanation because phase-04 produced no phase-handoff.md.


Phase-01 — Protected-path domain logic

Verdict: conformant

  • objective: Delivered completely. src/domain/security/protectedPaths.ts exports all four specified symbols (CLAUDE_PROTECTED_PREFIXES, isProtectedPath, resolveProtectedApprovals, decideProtectedPathApproval) plus the matching input/result interfaces.
  • excluded-scope: No schema, port, or app/infra code was touched. Scope respected.
  • files: Both planned files created (protectedPaths.ts, tests/unit/protectedPaths.test.ts). No deviations.
  • tests: Unit tests present covering all specified scenarios: isProtectedPath (exclusion of .claude/worktrees/, normalization, non-protected returns false), resolveProtectedApprovals (partition, empty config, dedup, prefix-with/without trailing slash), decideProtectedPathApproval (allow on exact-match Edit/Write/MultiEdit, defer on mismatch/non-edit/missing path).
  • boundaries: Pure module — no I/O imports. Consumer contracts (absolute paths for approved, repo-relative for uncovered) documented in handoff.
  • commit: Subject matches plan exactly: feat(security): add protected-path approval domain logic.
  • handoff: Complete. Covers exact signatures, normalization rules, approved/uncovered path conventions, tool-name set, and no deviations.

Phase-02 — Config field and security policy

Verdict: conformant-with-deviations

  • objective: Delivered completely. allowWriteProtected added to FilesystemConfigSchema (optional, array of non-empty strings), to ResolvedSecurityConfig.filesystem (required, default []), to SecurityPolicy.filesystem (required), and carried through resolveSecurityPolicy (secure branch: config value; unsafe branch: hardcoded []). phax.schema.json hand-edited (no generate script exists).
  • excluded-scope: No domain logic (phase-01) or port/app/infra (phases 03–04) code was touched. Scope respected.
  • files:
    • Unplanned — src/domain/security/resolveReviewPolicy.ts: this file also returns SecurityPolicy; adding allowWriteProtected as a required field to the interface forced an update here. Justified in handoff (hardcoded []; review phase never grants protected writes).
    • Unplanned — tests/unit/security/resolvePolicy.test.ts: existing ResolvedSecurityConfig fixture literals were missing the new required field; TypeScript enforced the update. Justified in handoff.
  • tests: tests/unit/resolvePolicyProtected.test.ts present covering config-resolution default, passthrough, schema-decode, and policy-resolution in secure vs. unsafe modes.
  • boundaries: SecurityPolicy.filesystem.allowWriteProtected is the declared producer for phase-03. Handoff confirms it carries raw prefix strings.
  • commit: Subject matches plan: feat(security): add allowWriteProtected to security config and policy.
  • handoff: Complete. Documents both unplanned edits, confirms phax.schema.json was hand-edited and why, and explicitly warns phase-03 to check its own fixtures for the same required-field pattern.

Phase-03 — Per-phase approvals, port, and preflight

Verdict: conformant-with-deviations

  • objective: Delivered completely. AgentRunOptions gains approvedProtectedPaths?. executePlan computes approvals from the declared file union, fails preflight with SecurityPreflightError on uncovered paths (secure mode only), passes approvedProtectedPaths into agent options, and records them in security.json. Preflight guard is correctly scoped to securityPolicy.mode === "secure".
  • excluded-scope: The hook and --settings wiring (phase-04) were not touched. Scope respected.
  • files:
    • Unplanned — src/schemas/securityPosture.ts: plan said "if schema-validated, update that schema too"; SecurityPosture is schema-validated, so this update is within the stated scope. Required approvedProtectedPaths field at top level. Justified in handoff.
    • Unplanned — ~36 test files: cascaded from phases 01–02 adding required fields (allowWriteProtected) to ResolvedSecurityConfig.filesystem and SecurityPolicy.filesystem, and from phase-03 adding approvedProtectedPaths to SecurityPosture. No logic changed in any of those files; the compiler enforced fixture updates. Comprehensively justified in handoff.
  • tests: tests/integration/protectedPathApprovals.test.ts present with three integration tests: grant (covered path reaches runAgent), preflight failure (uncovered path → SecurityPreflightError, backend never called), non-protected files (empty approvals, no preflight trip). Correct layer (integration with fake backend).
  • boundaries: AgentRunOptions.approvedProtectedPaths?: readonly string[] | undefined is the declared producer for phase-04. Handoff confirms absent/empty leaves invocation unchanged.
  • commit: Subject matches plan: feat(app): compute and transport per-phase protected-path approvals.
  • handoff: Complete. Documents SecurityPreflightError message format, the schema-postured update, the cascade of fixture updates, and the resume-path behavior.

Phase-04 — Claude approval hook and --settings wiring

Verdict: conformant-with-deviations

  • objective: Delivered. src/schemas/claudeHookPayload.ts decodes the PreToolUse payload. src/cli/commands/approveProtectedPath.ts is a thin hook command backed by the phase-01 domain decision. src/infra/providers/protectedPathHookSettings.ts is a pure settings builder. claudeCode.ts writes the settings file and appends --settings only when approvedProtectedPaths is non-empty.
  • excluded-scope: Codex and vibe adapters untouched. Scope respected.
  • files:
    • Missing (planned) — src/cli/main.ts: the plan specified registering the hidden subcommand in main.ts. Grep confirms registration happened in src/cli/program.ts at line 390 (__approve-protected-path) instead. The objective is met through an equivalent file; program.ts is the actual program builder in this CLI structure. However, no handoff explains this substitution (see handoff dimension below).
    • Unplanned — src/cli/program.ts: the actual file where the subcommand was registered.
    • Unplanned — src/cli/introspect.ts: touched but unplanned; purpose unknown without handoff.
    • Unplanned — scripts/generate-usage-spec.ts: touched but unplanned; likely related to CLI introspection or spec generation. Requires operator attention.
    • Unplanned — tests/integration/cliProgram.test.ts: touched but unplanned; likely covers the new subcommand registration.
  • tests: tests/integration/claudeProtectedPathHook.test.ts present. Per the explore agent: covers buildProtectedPathHookSettings output shape, env-var encoding, writeProtectedPathSettings file generation, payload parsing with extra-field tolerance, and decideProtectedPathApproval via hook for all three tools plus deferral cases.
  • boundaries: claudeCode.ts consumes approvedProtectedPaths from AgentRunOptions; no other adapter touched. Pure settings builder tested independently.
  • commit: Subject matches plan: feat(claude): grant declared protected-path edits via a scoped PreToolUse hook.
  • handoff: Not produced. The plan's required handoff content (settings file path/name, hook shape, path-passing mechanism, hidden subcommand name, confirmation that empty approvals leave invocation unchanged) is entirely absent. This is the most significant gap in the run.

Phase-05 — Documentation

Verdict: conformant-with-deviations

  • objective: Partially delivered. docs/security/protected-path-edits.md created and committed with complete content (protected paths explained, headless limitation, PreToolUse hook approach, trust model, preflight enforcement, provider scope). The .agents/skills/phax-planning/SKILL.md edit was performed in-session but not committed before phax took the gate snapshot.
  • excluded-scope: No code changes. Scope respected. .claude/skills/phax-planning/SKILL.md correctly not touched (it is a protected path and this run pre-dates the feature; plan marks it optional).
  • files:
    • Missing (planned) — .agents/skills/phax-planning/SKILL.md: marked as missing in reconciliation. The edit exists in the working tree (grep finds no allowWriteProtected terms in the current committed file, confirming the change is uncommitted). Handoff explains: phax took the commit snapshot before the formatter's final pass, so the staged edit was not captured.
  • tests: Docs only — none required.
  • boundaries: No architectural boundary crossed.
  • commit: Subject matches plan: docs(security): document scoped protected-path edit grants.
  • handoff: Complete and candid. Clearly distinguishes between committed and uncommitted state, calls out both manual follow-ups (commit the working-tree .agents change; sync .claude mirror after feature merge and phax.json opt-in).

Unplanned-change ledger

File Touched in Justification
src/domain/security/resolveReviewPolicy.ts phase-02 Required field added to SecurityPolicy interface; hardcoded [] — review phase never grants protected writes. Justified in handoff.
tests/unit/security/resolvePolicy.test.ts phase-02 Fixture literals missing new required field; TypeScript enforced update. Justified in handoff.
src/schemas/securityPosture.ts phase-03 Plan said "if schema-validated, update that schema too"; SecurityPosture is schema-validated. Justified in handoff.
~36 test files (integration + unit) phase-03 Required-field cascade from phases 01–02 forced fixture updates across the test suite; no logic changes. Justified in handoff.
src/cli/program.ts phase-04 Actual file where hidden subcommand was registered instead of planned main.ts. Not justified (no handoff).
src/cli/introspect.ts phase-04 Purpose unknown — no handoff. Requires operator inspection.
scripts/generate-usage-spec.ts phase-04 Purpose unknown — no handoff. Requires operator inspection.
tests/integration/cliProgram.test.ts phase-04 Likely covers CLI program structure changes. No handoff to confirm scope.

Unmet-promise ledger

Artifact Phase Status
.agents/skills/phax-planning/SKILL.md committed edit phase-05 Edit exists in working tree; not in commit. Follow-up commit required.
Phase-04 handoff phase-04 Not produced. Required handoff content (settings shape, path-passing mechanism, subcommand name, empty-path behavior confirmation) is undocumented.

Attention points

  1. Commit the .agents/skills/phax-planning/SKILL.md change. The edit (protected-path guidance subsection) exists in the working tree but was not captured in the phase-05 commit. Stage and commit on main before merging.

  2. Sync .claude/skills/phax-planning/SKILL.md after merge. Once phax.json has security.filesystem.allowWriteProtected: [".claude/skills/"] and this feature is live, run phax with that config to have the agent sync the protected mirror automatically — or copy manually.

  3. Inspect phase-04 unplanned files. scripts/generate-usage-spec.ts and src/cli/introspect.ts were touched without a handoff explanation. Confirm these changes are intentional and consistent with the rest of the codebase before merging.

  4. Phase-04 handoff gap. The missing phase-handoff.md means the settings file path, hook shape, env-var convention, and subcommand name are not on record. If these are needed by a downstream phase or operator, reconstruct from the committed code (src/infra/providers/protectedPathHookSettings.ts and src/cli/commands/approveProtectedPath.ts).

  5. src/cli/main.ts vs src/cli/program.ts. The subcommand was registered in program.ts (line 390: __approve-protected-path), not main.ts as planned. Confirm this is the correct registration point for hidden subcommands in this CLI, then update any future plan templates that reference main.ts for subcommand registration.

Phase details

phase-01 — Protected-path domain logic

File reconciliation

PHAX File Reconciliation

Planned to create

  • src/domain/security/protectedPaths.ts
  • tests/unit/protectedPaths.test.ts

Summary: No deviations from the planned file lists.

Phase handoff

What was delivered

  • src/domain/security/protectedPaths.ts — pure domain module exporting
    CLAUDE_PROTECTED_PREFIXES, isProtectedPath,
    resolveProtectedApprovals, decideProtectedPathApproval, plus the input/
    result interfaces.
  • tests/unit/protectedPaths.test.ts — unit coverage for the constant,
    isProtectedPath (including .claude/worktrees/ exclusion, normalization,
    and .. rejection), resolveProtectedApprovals (partition, empty-config,
    non-protected ignore, dedup, prefix-with/without trailing slash), and
    decideProtectedPathApproval (allow on Edit/Write/MultiEdit exact match,
    defer on mismatch / non-edit tool / missing or relative filePath,
    normalization of redundant segments).

Key decisions and why

  • approved paths are returned absolute POSIX (joined with
    worktreeRoot); uncovered paths stay repo-relative POSIX. Reason:
    Claude's runtime hook payload carries an absolute file_path for exact
    comparison, while preflight errors read cleaner with the path the operator
    typed.
  • Prefix matching is plain string compare (bare or bare + "/"). No glob
    dependency. A configured prefix with or without trailing / matches the
    same set.
  • .claude/worktrees/ exclusion lives in a separate
    CLAUDE_PROTECTED_EXCLUSIONS constant and is checked before the
    inclusion list, so any future protected prefix automatically inherits the
    same exclusion semantics.
  • Bare .claude (no trailing path) reports as protected — matches the
    spec's "equals prefix sans trailing slash" rule.
  • Runtime decision requires path.posix.isAbsolute(filePath) and a tool in
    { Edit, Write, MultiEdit }; everything else is "defer" (the hook emits
    nothing and Claude's default behavior takes over).

Exact locations (file paths and exported names)

  • src/domain/security/protectedPaths.ts
    • CLAUDE_PROTECTED_PREFIXES: readonly string[]
    • isProtectedPath(repoRelativePosixPath: string): boolean
    • resolveProtectedApprovals(input: ResolveProtectedApprovalsInput): ResolveProtectedApprovalsResult
    • decideProtectedPathApproval(input: DecideProtectedPathApprovalInput): "allow" | "defer"
    • Types: ResolveProtectedApprovalsInput, ResolveProtectedApprovalsResult,
      DecideProtectedPathApprovalInput
  • tests/unit/protectedPaths.test.ts

What the next phase needs to know

  • Phase-03 must pass worktreeRoot as a POSIX-style absolute path; trailing
    / is stripped and \ is converted, but it must already be absolute (the
    module does not call process.cwd()).
  • Non-protected planned paths are silently dropped by
    resolveProtectedApprovals — phase-03 can pass the full union of
    plannedFilesToCreate ∪ plannedFilesToEdit ∪ optionalFilesToEdit without
    pre-filtering.
  • Phase-04's generated hooks.PreToolUse matcher must be exactly
    "Edit|Write|MultiEdit", matching the APPROVABLE_TOOL_NAMES set used at
    decision time.
  • Phase-04 must hand each approved absolute path through to the hook command
    verbatim; no re-globbing — decision is exact-string match after
    path.posix.normalize.
  • For unsafe mode, phase-02 should pass allowWriteProtected: []; the
    module will then yield empty approved and put every protected planned
    path into uncovered, which phase-03's preflight will turn into a
    SecurityPreflightError.
  • No file-plan deviations.

phase-02 — Config field and security policy

File reconciliation

PHAX File Reconciliation

Planned to create

  • tests/unit/resolvePolicyProtected.test.ts

Planned to edit

  • src/schemas/securityConfig.ts
  • src/domain/security/types.ts
  • src/domain/security/resolvePolicy.ts
  • phax.schema.json

Unplanned files edited

Deviation — agent must explain in phase-handoff.md under "What the next phase needs to know".

  • src/domain/security/resolveReviewPolicy.ts
  • tests/unit/security/resolvePolicy.test.ts

Summary: Deviations detected — see sections above.

Phase handoff

What was delivered

  • src/schemas/securityConfig.tsFilesystemConfigSchema gains optional allowWriteProtected: Schema.Array(Schema.NonEmptyString); ResolvedSecurityConfig.filesystem gains required allowWriteProtected: readonly string[] defaulting to [] in resolveSecurityConfig.
  • src/domain/security/types.tsSecurityPolicy.filesystem gains required allowWriteProtected: readonly string[].
  • src/domain/security/resolvePolicy.ts — secure branch passes config.filesystem.allowWriteProtected through; unsafe branch hardcodes [].
  • phax.schema.jsonsecurity.filesystem.allowWriteProtected property added (array of NonEmptyString) with a description.
  • tests/unit/resolvePolicyProtected.test.ts — new unit tests for config-resolution (default, passthrough, schema-decode) and policy-resolution (secure, unsafe).

Key decisions and why

  • allowWriteProtected is always [] in unsafe mode: unsafe mode drops the entire jail, so the hook is irrelevant and should never be generated. Carrying config's value into unsafe would be misleading.
  • phax.schema.json was hand-edited — no generate:schema script exists in the repo. The edit mirrors the Effect schema exactly.

Exact locations (file paths and exported names)

  • src/schemas/securityConfig.tsFilesystemConfigSchema, ResolvedSecurityConfig (interface with filesystem.allowWriteProtected: readonly string[]), resolveSecurityConfig
  • src/domain/security/types.tsSecurityPolicy (interface with filesystem.allowWriteProtected: readonly string[])
  • src/domain/security/resolvePolicy.tsresolveSecurityPolicy(input: ResolvePolicyInput): SecurityPolicy
  • tests/unit/resolvePolicyProtected.test.ts — new test file

What the next phase needs to know

  • policy.filesystem.allowWriteProtected is the readonly string[] of operator-opted-in path prefixes. Phase-03 passes it verbatim to resolveProtectedApprovals from phase-01 (the function expects the raw prefix strings as stored in phax.json, e.g. ".claude/skills/", not absolute paths).
  • Unplanned edit — src/domain/security/resolveReviewPolicy.ts: this file also returns SecurityPolicy and had to be updated to include allowWriteProtected: []. The field is required on the interface; omitting it would have caused a type error. The value is hardcoded [] — the review phase never grants protected-path writes.
  • Unplanned edit — tests/unit/security/resolvePolicy.test.ts: the existing ResolvedSecurityConfig fixture literals were missing the new required allowWriteProtected field and would have caused type errors. Updated all four fixtures to include allowWriteProtected: [].
  • Any other file in the codebase that constructs a ResolvedSecurityConfig or SecurityPolicy literal must now include allowWriteProtected: []. Phase-03 should check its test fixtures for the same pattern.

phase-03 — Per-phase approvals, port, and preflight

File reconciliation

PHAX File Reconciliation

Planned to create

  • tests/integration/protectedPathApprovals.test.ts

Planned to edit

  • src/ports/backend.ts
  • src/app/executePlan.ts

Unplanned files edited

Deviation — agent must explain in phase-handoff.md under "What the next phase needs to know".

  • src/schemas/securityPosture.ts
  • tests/e2e/gateExhaustionResume.test.ts
  • tests/e2e/resetPhase.test.ts
  • tests/e2e/semanticTrace.providers.test.ts
  • tests/e2e/semanticTrace.test.ts
  • tests/integration/adjustPlanCommand.test.ts
  • tests/integration/enter.test.ts
  • tests/integration/enterPhase.test.ts
  • tests/integration/executePlan.test.ts
  • tests/integration/perPhaseBranch.test.ts
  • tests/integration/plansOverlapCommand.test.ts
  • tests/integration/plansOverlapLanded.test.ts
  • tests/integration/providerDispatcher.test.ts
  • tests/integration/rateLimit.test.ts
  • tests/integration/reconciliation.test.ts
  • tests/integration/resume.test.ts
  • tests/integration/resumeFromCleanup.test.ts
  • tests/integration/resumeFromCommit.test.ts
  • tests/integration/resumeHandoff.test.ts
  • tests/integration/reviewCodeCommand.test.ts
  • tests/integration/reviewCompliance.test.ts
  • tests/integration/reviewComplianceCommand.test.ts
  • tests/integration/routing.test.ts
  • tests/integration/runFolder.test.ts
  • tests/integration/sessionInfo.test.ts
  • tests/integration/setupFailure.test.ts
  • tests/integration/stateMachineContract.test.ts
  • tests/integration/telemetry/end-to-end.test.ts
  • tests/unit/dryRun.test.ts
  • tests/unit/gateProfile.test.ts
  • tests/unit/providers/claudeCode.test.ts
  • tests/unit/providers/codexCli.test.ts
  • tests/unit/providers/mistralVibe.test.ts
  • tests/unit/security/capabilities.test.ts
  • tests/unit/security/posture.test.ts
  • tests/unit/security/resolveReviewPolicy.test.ts

Summary: Deviations detected — see sections above.

Phase handoff

What was delivered

  • src/ports/backend.tsAgentRunOptions gains approvedProtectedPaths?: readonly string[] | undefined, documented like agentCommands.
  • src/app/executePlan.ts — imports resolveProtectedApprovals; for each new phase computes approvals from declared planned paths + resolved policy, fails preflight with SecurityPreflightError on uncovered protected paths (secure mode only), passes approvedProtectedPaths into agentOptions, and records it in security.json via the extended posture shape.
  • src/schemas/securityPosture.tsSecurityPostureSchema and SecurityPosture type gain a required approvedProtectedPaths: Schema.Array(Schema.String) field at the top level.
  • tests/integration/protectedPathApprovals.test.ts — three integration tests: grant (secure mode, covered path passes approvedProtectedPaths to runAgent), uncovered preflight failure (secure mode, empty allowWriteProtectedSecurityPreflightError before backend call), non-protected files (no preflight, empty approvals).

Key decisions and why

  • Preflight runs in secure mode only. resolveSecurityPolicy hardcodes allowWriteProtected: [] for unsafe/isolated modes, so every declared protected path would be "uncovered" and always trip preflight. Since protected paths are a Claude Code secure-mode concept (no jail in unsafe mode), the guard is securityPolicy.mode === "secure". Tests that use securityMode: "secure" override are needed to exercise the grant and preflight paths.
  • approvedProtectedPaths at SecurityPosture top level, not inside filesystem. SecurityPosture.filesystem only carries allowRead/allowWrite; adding allowWriteProtected to that struct would have been wrong.
  • Field is required in SecurityPostureSchema. Consistent with the project's no-back-compat-shims convention; existing security.json consumers must be updated.
  • Resume path skips preflight. The phase was already preflighted at launch. The resume branch recomputes approvedProtectedPaths from the current policy (same logic, no preflight guard) so the approved set is fresh if config changed.

Exact locations (file paths and exported names)

  • src/ports/backend.tsAgentRunOptions.approvedProtectedPaths?: readonly string[] | undefined
  • src/app/executePlan.ts — imports resolveProtectedApprovals from ../domain/security/protectedPaths.js; preflight guard: securityPolicy.mode === "secure" && protectedApprovals.uncovered.length > 0; passes approvedProtectedPaths: protectedApprovals.approved in agentOptions
  • src/schemas/securityPosture.tsSecurityPostureSchema, SecurityPosture (type), encodeSecurityPosture, decodeSecurityPosture
  • tests/integration/protectedPathApprovals.test.ts"executePlan — protected-path approvals" describe block

What the next phase needs to know

  • Phase-04 reads options.approvedProtectedPaths from AgentRunOptions in claudeCode.ts. Absent or empty (undefined or length 0) must leave the invocation byte-identical to today — no --settings appended.
  • The SecurityPreflightError message format: "Security preflight failed: phase \"${id}\" declares ${N} protected path(s) not covered by security.filesystem.allowWriteProtected in phax.json." with missing = repo-relative string array.
  • Unplanned: src/schemas/securityPosture.ts — the plan said "extend the record shape; if it is schema-validated, update that schema too." This is the schema-validated update; not a deviation from intent.
  • Unplanned: ~36 test files — phases 01–02 added required fields (allowWriteProtected: readonly string[]) to ResolvedSecurityConfig.filesystem and SecurityPolicy.filesystem, and this phase added a required approvedProtectedPaths to SecurityPosture. All existing test fixtures that construct these types as literals had to be updated; the compiler enforces this. No logic changed in those files.
  • Any phase-04 test that constructs a SecurityPosture or ResolvedConfig.security literal must include allowWriteProtected: [] and approvedProtectedPaths: [].

phase-04 — Claude approval hook and --settings wiring

File reconciliation

PHAX File Reconciliation

Planned to create

  • src/schemas/claudeHookPayload.ts
  • src/cli/commands/approveProtectedPath.ts
  • src/infra/providers/protectedPathHookSettings.ts
  • tests/integration/claudeProtectedPathHook.test.ts

Planned to edit

  • src/cli/main.ts
  • src/infra/providers/claudeCode.ts

Unplanned files edited

Deviation — agent must explain in phase-handoff.md under "What the next phase needs to know".

  • scripts/generate-usage-spec.ts
  • src/cli/introspect.ts
  • src/cli/program.ts
  • tests/integration/cliProgram.test.ts

Summary: Deviations detected — see sections above.

Phase handoff

PARTIAL — phase-handoff.md missing for phase-04


phase-05 — Documentation

File reconciliation

PHAX File Reconciliation

Planned to create

  • docs/security/protected-path-edits.md

Planned to edit

  • .agents/skills/phax-planning/SKILL.md

Summary: Deviations detected — see sections above.

Phase handoff

What was delivered

  • docs/security/protected-path-edits.md — new security doc explaining Claude Code's protected paths, why headless acceptEdits cannot reach them, the PreToolUse hook approach, the allowWriteProtected operator opt-in, and the plan-declares/config-grants trust model.

Key decisions and why

  • .agents/skills/phax-planning/SKILL.md was edited in the session (added a "Protected-path edits" subsection under "Required PHAX security configuration changes") but the change was not captured in the phase commit. phax took the commit snapshot before the formatter ran its final pass. The skill guidance exists in the working tree but is not part of this commit.
  • .claude/skills/phax-planning/SKILL.md was intentionally not edited — it is a protected path and this run pre-dates the feature. The plan explicitly calls it optional and records it as a manual follow-up.

Exact locations (file paths and exported names)

  • docs/security/protected-path-edits.md — feature explanation for operators and plan authors (in the committed phase-05 diff)
  • .agents/skills/phax-planning/SKILL.md — skill guidance including protected-path section (uncommitted working-tree change; needs a follow-up commit or manual staging)
  • .claude/skills/phax-planning/SKILL.md — protected mirror; still unsynced from .agents

What the next phase needs to know

  • This is the final phase of the run; there is no code-phase successor.
  • Deviation: .agents/skills/phax-planning/SKILL.md is listed as planned-to-edit but absent from the commit. The edit exists in the working tree (added "Protected-path edits" guidance). It was not staged before phax took the gate snapshot. A follow-up commit on main should stage and commit this file alone.
  • Manual follow-up (two items):
    1. Commit the uncommitted .agents/skills/phax-planning/SKILL.md change.
    2. Once phax.json has security.filesystem.allowWriteProtected: [".claude/skills/"] configured and the feature is merged, sync .claude/skills/phax-planning/SKILL.md from the .agents mirror (a subsequent phax run can do this automatically using the new feature).
  • All gates passed against the committed state (typecheck, lint, format:check, knip, test, audit:architecture, build, deno:smoke, deno:smoke-binary).

lbdremy added 5 commits June 30, 2026 13:26
Add src/domain/security/protectedPaths.ts: a pure module that detects Claude-Code protected paths, resolves a phase's declared protected paths against operator-configured allowWriteProtected prefixes (returning approved absolute paths and uncovered violations), and decides a single PreToolUse approval at runtime. No I/O. Covered by unit tests for detection, resolution, and the runtime decision.

---

Run-Id: protected-path-approval-1782818550964
Short-Name: protected-path-approval
Phase-Id: phase-01
Phase-Title: Protected-path domain logic
Model: claude-opus-4-8
Effort: medium
Worktree: /Users/remyloubradou/.phax/worktrees/phax.protected-path-approval/phase-01
Session-Id: ac333995-736d-44b6-bf21-79524d07e301
Gate-Log: /Users/remyloubradou/.phax/runs/phax.protected-path-approval/phase-01/checks-attempt-01.log
Add an optional security.filesystem.allowWriteProtected string array to the phax.json security schema, thread it through ResolvedSecurityConfig (default []) into the secure SecurityPolicy, and mirror it in phax.schema.json. This is the operator opt-in that scopes which protected path-prefixes a run may be granted to edit. Covered by config-resolution and policy-resolution unit tests.

---

Run-Id: protected-path-approval-1782818550964
Short-Name: protected-path-approval
Phase-Id: phase-02
Phase-Title: Config field and security policy
Model: claude-sonnet-4-6
Effort: medium
Worktree: /Users/remyloubradou/.phax/worktrees/phax.protected-path-approval/phase-02
Session-Id: 3e4bf936-bcb2-4289-abd1-057bb5195de6
Gate-Log: /Users/remyloubradou/.phax/runs/phax.protected-path-approval/phase-02/checks-attempt-01.log
Compute each phase's approvedProtectedPaths from its planned files and the resolved allowWriteProtected prefixes, pass them through a new optional AgentRunOptions field to the backend, fail preflight when a phase declares a protected path the operator has not opted into, and record approvals in security.json. Covered by integration tests for the granted, uncovered, and non-protected cases.

---

Run-Id: protected-path-approval-1782818550964
Short-Name: protected-path-approval
Phase-Id: phase-03
Phase-Title: Per-phase approvals, port, and preflight
Model: claude-sonnet-4-6
Effort: medium
Worktree: /Users/remyloubradou/.phax/worktrees/phax.protected-path-approval/phase-03
Session-Id: bcfbf63b-2935-4ec2-8cf0-59e7a22a1e4d
Gate-Log: /Users/remyloubradou/.phax/runs/phax.protected-path-approval/phase-03/checks-attempt-01.log
…lUse hook

Generate a PreToolUse approval hook scoped to a phase's approvedProtectedPaths and pass it to claude via --settings, so the agent can edit declared .claude/** files while the rest of the secure jail stays intact. Adds a stdin-payload schema, a thin hidden CLI hook command backed by the phase-01 domain decision, and a pure settings builder; the adapter writes the settings file and appends --settings only when approvals exist. Claude-only; codex/vibe unchanged. Covered by settings-builder, arg, and hook-decision tests.

---

Run-Id: protected-path-approval-1782818550964
Short-Name: protected-path-approval
Phase-Id: phase-04
Phase-Title: Claude approval hook and `--settings` wiring
Model: claude-sonnet-4-6
Effort: high
Worktree: /Users/remyloubradou/.phax/worktrees/phax.protected-path-approval/phase-04
Session-Id: 0ea56e67-12ce-4ad6-b488-6294e2586658
Gate-Log: /Users/remyloubradou/.phax/runs/phax.protected-path-approval/phase-04/checks-attempt-01.log
Document the protected-path approval feature: how declaring a .claude/** file in a phase plus a security.filesystem.allowWriteProtected opt-in in phax.json grants a scoped edit, why Claude Code's protected paths block headless acceptEdits, and the plan-declares/config-grants trust model. Updates the .agents phax-planning skill mirror and adds docs/security/protected-path-edits.md; the .claude mirror is a noted manual follow-up.

---

Run-Id: protected-path-approval-1782818550964
Short-Name: protected-path-approval
Phase-Id: phase-05
Phase-Title: Documentation
Model: claude-sonnet-4-6
Effort: low
Worktree: /Users/remyloubradou/.phax/worktrees/phax.protected-path-approval/phase-05
Session-Id: 3f3bfe69-8c08-45aa-8e13-244b2c30531f
Gate-Log: /Users/remyloubradou/.phax/runs/phax.protected-path-approval/phase-05/checks-attempt-01.log
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant