Security

PHAX applies provider-native execution boundaries to keep agents safe by default. This document describes the security modes, configuration, and how to verify your setup.

Overview

PHAX supports three security modes:

ModeDescription
secureStrongest available provider-native sandboxing. Filesystem access limited to worktree and state root, network governed by the network.profile (enforced only where the provider supports it — see Provider Capabilities), MCP disabled by default. This is the default.
unsafeHost-unrestricted access (legacy behavior). Use with caution.
isolatedPlanned external sandbox mode. Not yet available.

Configuration

Add a security block to your phax.json:

{
  "version": 1,
  "security": {
    "profile": "secure",
    "filesystem": {
      "allowRead": ["/additional/read/path"],
      "allowWrite": ["/additional/write/path"]
    },
    "network": {
      "profile": "provider-only",
      "allowDomains": ["example.com"]
    },
    "mcp": {
      "mode": "disabled",
      "allow": ["/path/to/mcp-server.json"]
    }
  }
}

Configuration Options

FieldTypeDefaultDescription
profile"secure" | "unsafe" | "isolated""secure"The security mode for the run
filesystem.allowReadstring[][]Additional read paths (added to worktree + state root)
filesystem.allowWritestring[][]Additional write paths (added to worktree + state root)
network.profile"provider-only" | "dev-allowlist" | "open""provider-only"Network restriction profile
network.allowDomainsstring[][]Additional allowed domains (added to provider API domain)
mcp.mode"disabled" | "local-only" | "allowlist" | "provider-default""disabled"MCP access mode
mcp.allowstring[][]Paths to MCP server config files passed to the agent via --mcp-config. Name-based allowlisting (server names like "nx-mcp") is not supported; phax validates that every entry resolves to a readable file before the run starts.

Network Profiles

The profile expresses intent; enforcement depends on the provider (see Provider Capabilities). No provider applies per-domain filtering; Codex is the only one that enforces a hard on/off egress boundary.

  • provider-only: Most conservative. Under Codex this disables subprocess network entirely; under Claude/Mistral it is recorded but not enforced as a domain filter.
  • dev-allowlist: Provider API domain plus any configured allowDomains, recorded in the policy. Under Codex this enables egress; no provider filters to the listed domains.
  • open: No network restrictions (not recommended).

MCP Modes

  • disabled: MCP is disabled entirely
  • local-only: Only local MCP servers are allowed
  • allowlist: Only MCP servers whose config files are listed in mcp.allow (as file paths) can be used
  • provider-default: Use the provider's default MCP behavior

CLI Override

Override the security mode from the command line:

phax run --security secure    # default, explicit
phax run --security unsafe    # legacy host-unrestricted behavior
phax run --security isolated  # stubbed: exits with error (not yet available)

The CLI flag takes precedence over the configuration file.

Provider Capabilities

PHAX applies the strongest available provider-native controls for each provider:

ProviderFilesystem JailNetwork controlMCP Allowlist
claude-codeStrongNone enforced (profile recorded)Supported
codex-cliStrongOn/off (sandbox egress toggle)Supported
mistral-vibePartialNoneSupported

No provider enforces a domain allowlist. network.allowDomains and the dev-allowlist profile are carried into the policy and recorded in security.json, but no provider applies per-domain filtering today. The only enforced network control is Codex's sandbox egress toggle: under provider-only, Codex sets network_access=false, blocking subprocess network entirely; dev-allowlist/open set it to true. Claude Code has no native domain-allowlist flag — only network.profile is carried — and Mistral Vibe has no network control at all.

Provider-Specific Behavior

Claude Code (claude-code):

  • Secure mode runs headless with --permission-mode acceptEdits (edits auto-approved within the working dirs, denied outside)
  • Filesystem restricted to configured paths via the worktree cwd + --add-dir
  • No native domain-allowlist flag; network.profile is recorded but not enforced as a per-domain filter
  • MCP disabled or allowlisted via --strict-mcp-config
  • Shell is denied by default; only the phase's gate commands are allowlisted (see Shell command execution)

Codex CLI (codex-cli):

  • Secure mode uses workspace-write sandbox
  • Writable roots limited to worktree + state root + configured paths
  • Network egress toggled on/off by network.profile (provider-only → off); no domain allowlist
  • Non-escaping approval mode
  • Shell runs inside the OS sandbox — any command is permitted but confined (see Shell command execution)

Mistral Vibe (mistral-vibe):

  • Filesystem isolation is partial (weaker than Claude/Codex)
  • Network controls are unsupported
  • When secure mode is requested, Vibe is skipped in strict routing unless it's the terminal fallback
  • Marked as "partially secured" with appropriate warnings
  • Shell tool calls are auto-approved, with no per-command allowlist (see Shell command execution)

Skill edit grants (Claude Code)

Claude Code treats .claude/ as a protected path and never auto-approves writes there. In a headless secure-mode session nobody can approve the prompt, so edits to .claude/skills/** are denied by default.

To let a phase edit skill files:

  • List each .claude/skills/... file in the phase's planned-file sections (create, edit, or optional).
  • Start the run with phax run --allow-skill-edits.

The phase, including its fix loop and handoff generation, may then edit or create exactly the declared files. Without the flag, a plan that declares skill files is refused before the run is created (exit code 11), with a message naming the phases and files, so re-running with the flag keeps the plan's run name. phax resume has no flag of its own: it inherits the consent recorded in the run's run-status.json, and a run without recorded consent is refused again.

Mechanism. The Claude adapter passes an inline --settings JSON with a PermissionRequest hook. There is one handler per declared file and per edit tool (Edit, Write, MultiEdit). Each handler is scoped by an absolute if permission rule and echoes an allow decision. The grants are recorded in security.json as skillEditGrants for every provider. Only the Claude adapter acts on them, because Codex and Vibe do not block .claude/. With no grants, the argv is unchanged.

Not covered:

  • Other .claude/ paths (.claude/settings.json, …) and other protected paths such as .git keep today's behavior.
  • Undeclared sibling files: a grant covers the exact file, not its directory. Declare every file, including references/*.md.
  • Glob paths: a declared path containing *, ?, [, ], (, ), { or } is dropped, as are absolute paths and paths with ...

Why not something simpler. A permissions.allow rule such as Edit(.claude/**) has no effect, because Claude checks protected paths before it reads allow rules. A PreToolUse hook that returns allow runs, but the write is still denied. Only a PermissionRequest allow unblocks it. Note that if matches by tool name, so Edit(...) does not match a Write call, which is why each file gets one handler per tool.

Shell command execution

The phase prompt and the gate fix-loop both instruct the agent to run the phase's gate commands (the resolved gate profile, e.g. pnpm typecheck, pnpm test) to verify — and fix — its own work. How that shell access is constrained differs by provider, because each provider exposes a different native control surface. The granularity differs, but in every secure-mode case the commands run confined to the worktree. The allowlisted set is every step's command in the profile, regardless of its firing — terminal steps still execute at the last phase, so their commands must be allowlisted from the start:

ProviderShell model in secure mode
claude-codePer-command allowlist. Bash is denied by default; phax allowlists exactly the gate commands (--allowedTools "Bash(<cmd>:*)"). Every other shell command is denied.
codex-cliOS-sandboxed execution. Any command may run, but the workspace-write sandbox confines writes to the writable roots and the network profile gates egress. There is no per-command allowlist surface.
mistral-vibeApproval-policy only. The auto-approve agent permits shell tool calls with no per-command allowlist; isolation rests on the partial filesystem jail (already reflected by the partial-filesystem mark).

Notes:

  • Claude's allowlist is exact: a pnpm format:check gate permits pnpm format:check, not pnpm format. If a phase needs a sibling command (such as a formatter's write variant), add it as its own gate entry.
  • Codex and Vibe permit broader command execution than Claude, but this is not a filesystem-isolation downgrade — codex keeps a strong OS sandbox, and Vibe's weaker isolation is already surfaced via its security mark. For that reason the broader shell surface is documented here rather than emitted as a separate mark.

Security Artifacts

Each phase writes a security.json artifact containing:

{
  "version": 1,
  "mode": "secure",
  "provider": "claude-code",
  "sandboxEnabled": true,
  "filesystem": {
    "allowRead": ["/path/to/worktree", "/home/user/.phax"],
    "allowWrite": ["/path/to/worktree", "/home/user/.phax"]
  },
  "network": {
    "profile": "provider-only",
    "allowDomains": ["api.anthropic.com"]
  },
  "mcp": {
    "mode": "disabled",
    "allow": []
  },
  "downgraded": false,
  "marks": [],
  "skillEditGrants": [],
  "providerSkippedForSecurity": []
}

These artifacts are:

  • Written to <run-path>/<phase-id>/security.json
  • Emitted as security.policy.applied semantic telemetry events
  • Included in the final-report.md under the Security section

Final Report Security Section

The final-report.md includes a Security section with:

  • Run-level security mode
  • Per-phase security posture table showing:
    • Mode, provider, sandbox status
    • Network profile and allowed domains
    • MCP mode and allowed servers
    • Downgrade status and marks
    • Providers skipped for security reasons
  • A Skill Edit Grants table listing, per phase, the .claude/skills/** files granted by --allow-skill-edits (omitted when nothing was granted)

Verification

Check which providers are installed and their security capabilities:

phax security status

This reports per-provider jail, network, and MCP capabilities from live probes.

Unsafe Mode Warning

When running in unsafe mode, PHAX prints a warning:

┌─────────────────────────────────────────────────────────────┐
│                    ⚠️  UNSAFE MODE WARNING                      │
│                                                                  │
│  Running in UNSAFE mode: the agent has HOST-UNRESTRICTED       │
│  access to:                                                        │
│    • Filesystem: full host read/write access                   │
│    • Network: no domain restrictions                             │
│    • MCP: no MCP server restrictions                             │
│    • Commands: can run any shell command                        │
│                                                                  │
│  This is NOT RECOMMENDED for production use.                   │
│  Use --security secure or remove --security unsafe.             │
└─────────────────────────────────────────────────────────────┘

Routing and Security

When secure mode is active, providers that cannot satisfy strict security requirements are skipped during routing:

  • Claude Code is the guaranteed strong baseline (never filtered)
  • Codex CLI is strong and supported
  • Mistral Vibe is partial and may be skipped in strict contexts

Skipped providers are recorded in RoutingResolution.skippedForSecurity and appear in the security artifact and final report.

Supply-chain and distribution security

The controls above govern the agent PHAX runs. PHAX also hardens its own distribution and dependency surface:

  • Verified binary installs. The npm wrapper (@lbdremy/phax) downloads a prebuilt binary from the matching GitHub release on first run. Before the binary is made executable or run, the installer fetches the published <name>.sha256 sidecar, recomputes the download's SHA-256, and refuses to run — deleting the file — on a fetch failure, malformed checksum, or mismatch. A tampered, corrupted, or misdirected release asset can never be executed silently (npm/bin/phax).
  • Reproducible release checksums. scripts/build-binaries.ts writes a sha256sum-compatible <name>.sha256 next to every release binary, and the release workflow publishes both. scripts/security/release-audit.sh re-verifies each binary against its sidecar.
  • npm provenance. The release job publishes with npm publish --provenance, producing a signed build-provenance attestation that links the package to the workflow run and source commit.
  • No install-time code execution. The npm wrapper defines no install/postinstall lifecycle scripts; nothing runs on npm install beyond fetching the verified binary on first invocation.
  • Argument-safe subprocess calls. Every subprocess PHAX spawns (git, gh, the provider CLIs) uses spawn/execFile with an argv array — never a shell string — so there is no shell-interpolation surface. External input is decoded through an Effect Schema before it reaches the domain.

Auditing the codebase (pnpm audit:security)

PHAX ships a reusable, dependency-free security-audit toolkit under scripts/security/:

pnpm audit:security          # deps + secrets + code + release
pnpm audit:security code     # run a single check

Reports are written to dist/security-audit/ (report.md, findings.jsonl, and any per-tool output). The process exit code is gated by FAIL_ON (high by default; med or none to widen/disable).

CheckWhat it inspects
depspnpm audit (production advisories), lockfile hygiene, optional SBOM
secretscommitted credentials — gitleaks if installed, else a built-in pattern scan
codeinjection / unsafe-exec / architecture-boundary patterns; optional semgrep deep pass
releasenpm publish surface, installer checksum verification, release-binary checksums, Actions pinning, provenance

Each check uses a professional scanner when present and falls back to a built-in check otherwise, so a fresh checkout can run the full audit with only bash, git, and pnpm. Install the optional scanners for deeper coverage:

brew install gitleaks semgrep osv-scanner syft

A reviewed false-positive allowlist for the secret scan lives in .gitleaks.toml. See scripts/security/README.md for the full toolkit reference.

Keeping security high

  • Run the audit in CI. Add a job that runs pnpm audit:security on pull requests, plus a weekly schedule to catch newly disclosed advisories against unchanged code. Install gitleaks/osv-scanner/semgrep in the runner for full depth. Start report-only (FAIL_ON=none) and move to FAIL_ON=high once the tree is clean, so a new CVE surfaces without blocking unrelated work.
  • Keep dependencies fresh. Enable Dependabot or Renovate for both pnpm and GitHub Actions. Dev/build tooling (esbuild, vite, vitest) is the largest advisory surface; upgrade it promptly and re-run pnpm audit:security deps.
  • Complete the tracked hardening follow-ups in docs/plans/2607020808-security-hardening-plan.md: pin GitHub Actions to commit SHAs, add -- separators / tighten BranchNameSchema in the git adapter, and convert scripts/docs-cli.ts to execFileSync.
  • Re-audit the release before publishing. Run pnpm deno:build-binaries then pnpm audit:security release to confirm the shipped binaries match their checksums and the publish surface is clean.
  • Rotate optional-tool coverage. Periodically run with SCAN_HISTORY=1 so gitleaks sweeps the full git history, not just the working tree.

Example CI job

Add to .github/workflows/ci.yml (report-only to start; drop FAIL_ON/continue-on-error once the tree is clean to make it blocking):

  security-audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "24"
          cache: "pnpm"
      - run: pnpm install
      - name: Security audit
        run: pnpm audit:security
        env:
          FAIL_ON: none   # report-only until the tree is clean, then make blocking
        continue-on-error: true

The toolkit runs with only bash/git/pnpm. For deeper coverage, add steps that install gitleaks, osv-scanner, and semgrep before the audit (e.g. the gitleaks/gitleaks-action, the google/osv-scanner-action, or an asset download pinned by version) — the toolkit auto-detects and uses them.

Best Practices

  1. Default to secure: The default secure mode provides the best available protection
  2. Review security artifacts: Check security.json and the Security section in final-report.md
  3. Limit additional paths: Only add necessary paths to filesystem.allowRead/allowWrite
  4. Use dev-allowlist sparingly: Prefer provider-only network profile
  5. Verify provider capabilities: Use phax security status to confirm your providers support the required controls
  6. Avoid unsafe mode: Only use unsafe for debugging or trusted local development
  7. Audit before releasing: Run pnpm audit:security (and the release check after building binaries) as part of the release checklist