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:
Configuration
Add a security block to your phax.json:
Configuration Options
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 configuredallowDomains, 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 entirelylocal-only: Only local MCP servers are allowedallowlist: Only MCP servers whose config files are listed inmcp.allow(as file paths) can be usedprovider-default: Use the provider's default MCP behavior
CLI Override
Override the security mode from the command line:
The CLI flag takes precedence over the configuration file.
Provider Capabilities
PHAX applies the strongest available provider-native controls for each provider:
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.profileis 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.gitkeep 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:
Notes:
- Claude's allowlist is exact: a
pnpm format:checkgate permitspnpm format:check, notpnpm 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:
These artifacts are:
- Written to
<run-path>/<phase-id>/security.json - Emitted as
security.policy.appliedsemantic telemetry events - Included in the
final-report.mdunder 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:
This reports per-provider jail, network, and MCP capabilities from live probes.
Unsafe Mode Warning
When running in unsafe mode, PHAX prints a warning:
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>.sha256sidecar, 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.tswrites asha256sum-compatible<name>.sha256next to every release binary, and the release workflow publishes both.scripts/security/release-audit.shre-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/postinstalllifecycle scripts; nothing runs onnpm installbeyond fetching the verified binary on first invocation. - Argument-safe subprocess calls. Every subprocess PHAX spawns (git, gh, the
provider CLIs) uses
spawn/execFilewith 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/:
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).
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:
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:securityon pull requests, plus a weekly schedule to catch newly disclosed advisories against unchanged code. Installgitleaks/osv-scanner/semgrepin the runner for full depth. Start report-only (FAIL_ON=none) and move toFAIL_ON=highonce the tree is clean, so a new CVE surfaces without blocking unrelated work. - Keep dependencies fresh. Enable Dependabot or Renovate for both
pnpmand GitHub Actions. Dev/build tooling (esbuild, vite, vitest) is the largest advisory surface; upgrade it promptly and re-runpnpm 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 / tightenBranchNameSchemain the git adapter, and convertscripts/docs-cli.tstoexecFileSync. - Re-audit the release before publishing. Run
pnpm deno:build-binariesthenpnpm audit:security releaseto confirm the shipped binaries match their checksums and the publish surface is clean. - Rotate optional-tool coverage. Periodically run with
SCAN_HISTORY=1sogitleakssweeps 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):
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
- Default to secure: The default
securemode provides the best available protection - Review security artifacts: Check
security.jsonand the Security section infinal-report.md - Limit additional paths: Only add necessary paths to
filesystem.allowRead/allowWrite - Use dev-allowlist sparingly: Prefer
provider-onlynetwork profile - Verify provider capabilities: Use
phax security statusto confirm your providers support the required controls - Avoid unsafe mode: Only use
unsafefor debugging or trusted local development - Audit before releasing: Run
pnpm audit:security(and thereleasecheck after building binaries) as part of the release checklist