> For AI agents: the complete documentation index is available at https://docs.phax.run/llms.txt, the full documentation bundle is available at https://docs.phax.run/llms-full.txt.

# phax 0.31.0 \{#phax-0310}

Changes since 0.30.0: [v0.30.0...v0.31.0](https://github.com/lbdremy/phax/compare/v0.30.0...v0.31.0).

## Highlights \{#highlights}

- **Protected paths: files no phase of a run may change** — A phase never lands a change to a file that decides whether its own work passes; what it cannot fix stays failing and reaches the operator.

## Upgrading \{#upgrading}

- **Records readers: `security.json` is version 3.** Every phase's `security.json` carries a required `protectedPaths`, between `skillEditGrants` and `providerSkippedForSecurity`: `null` when `phax.json` declares none, otherwise `{ globs, enforcement }` with `enforcement` one of `enforced`, `partial` or `commit-check-only`. phax reads version 1 and 2 files as version 3.
- **Records readers: `gate-attribution.json` is stamped 0.31.0.** It carries a required `protectedPaths`: `null`, or `{ result, changes }` (a `pass` with no change, or a `fail` with at least one `{ path, kind, entry }`). 0.26.0 documents and older still read, as `null`. An older `@lbdremy/phax-schemas` cannot read 0.31.0 attributions.

## What's new \{#whats-new}

### Protected paths: files no phase of a run may change \{#protected-paths-files-no-phase-of-a-run-may-change}

> A phase never lands a change to a file that decides whether its own work passes; what it cannot fix stays failing and reaches the operator.

- A project lists, in phax.json, the files whose change is the operator's decision, and no phase of a run may land a change to them. `phax.json protectedPaths`

  Now:

  ```text
  {
    "version": 1,
    "gateProfiles": {
      "default": [{ "command": "steme audit" }]
    },
    "protectedPaths": ["**/steme.baseline.json"],
    "security": { "profile": "secure" }
  }
  ```

  :::details Before

  ```text
  {
    "version": 1,
    "gateProfiles": {
      "default": [{ "command": "steme audit" }]
    },
    "security": { "profile": "secure" }
  }
  ```

  :::

- Running `phax validate` lists the protected paths and refuses a malformed list, one set outside phax.json, or a glob inside a submodule. `phax validate`

  Now:

  ```text
  $ phax validate
  ✓ config is valid (project: steme-lab)
    project: /work/steme-lab/phax.json
    local:   (none)
    global:  (none)
    protected paths: **/steme.baseline.json, phax.json

  $ phax validate          # phax.json: "protectedPaths": ["../shared/allow.json"]
  Config validation failed: protectedPaths[0]: "../shared/allow.json" has a .. segment
    at: /work/steme-lab/phax.json
  $? = 2

  $ phax validate          # phax.local.json: "protectedPaths": []
  Config validation failed: "protectedPaths" is accepted only in phax.json
    at: /work/steme-lab/phax.local.json
  $? = 2

  $ phax validate          # phax.json: "protectedPaths": ["vendor/lint-rules/allow.json"]; .gitmodules: vendor/lint-rules
  Config validation failed: protectedPaths[0]: "vendor/lint-rules/allow.json" is inside the submodule vendor/lint-rules
    at: /work/steme-lab/phax.json
  $? = 2
  ```

  :::details Before

  ```text
  $ phax validate
  ✓ config is valid (project: steme-lab)
    project: /work/steme-lab/phax.json
    local:   (none)
    global:  (none)
  ```

  :::

- During a run the operator sees a gate attempt fail on each protected file the phase changed, and an exhausted phase names the files left for them to decide, the agent's last fix transcript and the two ways on. `phax run`

  Now:

  ```text
  ✗ phase-05 gate attempt 3 failed: protected paths changed
      packages/rust/steme-formats/steme.baseline.json  edited  (protectedPaths[0] **/steme.baseline.json)
  This run paused because its gate checks failed after exhausting all fix attempts.
  Protected paths this phase changed, which no phase may change:
    packages/rust/steme-formats/steme.baseline.json (edited)
  The agent's last fix attempt: ~/.phax/runs/steme-lab.audit-legs/phase-05/fix-attempt-02.jsonl
  They are yours to decide, outside this run's phases. Either put each file back to its
  content at the phase base and run `phax resume audit-legs --yes`, or make the change
  on the base branch and start a new run.
  $? = 4
  ```

  :::details Before

  ```text
  ✗ phase-05 gate attempt 3 failed: steme audit (exit 1)
  This run paused because its gate checks failed after exhausting all fix attempts.
  Fix the gate manually in the worktree, then resume — the gate is re-run first on
  resume, and if it passes the phase commits with no fresh agent invocation.
  $? = 4
  ```

  :::

- A phase's security record shows the protected paths and how much of the list its provider refused natively, or `null` when the project declares none. `<run>/<phase-id>/security.json`

  Now:

  ```text
  {
    "version": 3,
    "mode": "secure",
    "provider": "mistral-vibe",
    "skillEditGrants": [],
    "protectedPaths": {
      "globs": ["**/steme.baseline.json", "phax.json"],
      "enforcement": "enforced"
    },
    "providerSkippedForSecurity": [],
    "stop": null
  }

  "protectedPaths": null        (no protectedPaths in phax.json)
  "enforcement": "enforced" | "partial" | "commit-check-only"
  ```

  :::details Before

  ```text
  {
    "version": 2,
    "mode": "secure",
    "provider": "mistral-vibe",
    "skillEditGrants": [],
    "providerSkippedForSecurity": [],
    "stop": null
  }
  ```

  :::

- A phase's gate attribution shows the protected-path check's result and each protected change, or `null` when the project declares none. `<run>/<phase-id>/gate-attribution.json`

  Now:

  ```text
  {
    "$schema": "https://docs.phax.run/schemas/gate-attribution/0.31.0.json",
    "phase": "phase-05",
    "steps": [
      { "command": "steme audit", "surface": "local", "result": "pass" }
    ],
    "protectedPaths": {
      "result": "fail",
      "changes": [
        {
          "path": "packages/rust/steme-formats/steme.baseline.json",
          "kind": "edited",
          "entry": "protectedPaths[0]"
        }
      ]
    }
  }

  "protectedPaths": null        (no protectedPaths in phax.json)
  "result": "pass" | "fail"     ("changes" is [] on pass)
  "kind": "created" | "edited" | "deleted" | "renamed"
  ```

  :::details Before

  ```text
  {
    "$schema": "https://docs.phax.run/schemas/gate-attribution/0.26.0.json",
    "phase": "phase-05",
    "steps": [
      { "command": "steme audit", "surface": "local", "result": "pass" }
    ]
  }
  ```

  :::

- The help of `phax run` and `phax validate` says how protected paths behave. `phax.usage.kdl`

  Now:

  ```text
  cmd "validate" {
      help "Validate phax.json and its user overlays without any side effects; pass --plan to also validate a phax-plan.json"
      long_help "… Lists the protected paths; protectedPaths is refused in phax.local.json and ~/.phax/config.json …"
  }
  cmd "run" {
      long_help "… A gate attempt fails when the phase changed a path listed in protectedPaths …"
  }
  ```

  :::details Before

  ```text
  cmd "validate" {
      help "Validate phax.json and its user overlays without any side effects; pass --plan to also validate a phax-plan.json"
      long_help "…"
  }
  ```

  :::

- The security guide gains a Protected paths section saying what each provider refuses natively and what the commit check catches. `docs/security.md`

  Now:

  ```text
  ### Skill edit grants (Claude Code)
  …
  ### Protected paths

  | Provider       | Native rule                                   | Enforcement            |
  | -------------- | --------------------------------------------- | ---------------------- |
  | `claude-code`  | `Edit(//…)` deny, literal and `**/` paths      | enforced or partial    |
  | `mistral-vibe` | `write_file`/`edit` denylist, literal and `**/` paths | enforced or partial |
  | `codex-cli`    | none (permission profiles: later)              | commit-check-only      |

  ### Shell command execution
  ```

  :::details Before

  ```text
  ### Skill edit grants (Claude Code)
  …
  ### Shell command execution
  ```

  :::
