# fx (Vercel Labs) Permissions & Control Model, Explained

How fx's ask/auto/yolo modes, wildcard permission rules, hardcoded model reviewer, repo-config lockdown, and macOS sandbox actually work — what each mechanism does, what none of them record, and where the native model ends.

# The fx control model, explained

[fx](https://github.com/vercel-labs/fx) is Vercel Labs' minimal coding agent — written in Zig, announced August 18, 2026, installed with `curl -fsSL fx.sh/setup.sh | bash`. Minimal does not mean uncontrolled: fx ships a complete native permission system with a deterministic rules grammar, a model reviewer in the approval loop, and a repo-config lockdown that several older harnesses should copy. This page is the reference: what each mechanism does — from the docs as of launch day, cross-checked against the open-source tree and the v0.0.3 binary where they disagree — and where the native model ends.

*This page covers fx's own controls. ACP governs fx through [`@agenticcontrolplane/fx`](https://www.npmjs.com/package/@agenticcontrolplane/fx), a protocol middleware for `fx acp` — details at the bottom. For the cross-harness picture, see [the controls comparison](/controls).*

## Three modes

| Mode | Behavior |
|---|---|
| `ask` | Sensitive calls prompt for approval, except those resolved by a rule or by the inspection grammar below |
| `auto` | **The default.** Rules first; unresolved sensitive calls go to an automatic model review, with a human fallback in interactive mode |
| `yolo` | fx permission checks off, no command sandbox — the run has the full authority of the launching process |

The mode is set three ways, in the usual precedence shape: `/permissions <mode>` in-session, `permission_mode` in `~/.fx/settings.json`, or the `FX_PERMISSION_MODE` env var. `/permissions reset` returns to `ask` and clears any session grants.

`yolo` requires `yolo_acknowledged` before it will run — a deliberate speed bump rather than a bare flag. What it doesn't leave behind is any record that the controls were off.

## The rules grammar

Policy is a nested JSON `permission` object in `~/.fx/settings.json`, user-global or per-workspace profile:

```json
{
  "permission": {
    "*": "ask",
    "bash": {
      "git *": "allow",
      "git push *": "deny"
    }
  }
}
```

Wildcard patterns map to `allow`, `deny`, or `ask`. Resolution is simple and stated: **last matching rule wins, and workspace rules beat user-global ones.** Rules are managed interactively via `/allowlist`.

Two design decisions here are worth crediting:

- **`/permissions remember <allow|deny> <tool-name> <arguments-json>`** stores an exact confirmed rule *without running the action*. That's the right shape for turning a one-off decision into policy: the rule matches precisely what you confirmed, not a broadened prefix, and you don't have to execute the command to record the verdict. `/permissions revoke <rule-id>` removes it.
- **Session grants don't outlive the session.** A "yes, and don't ask again" answer is in-memory only — not persisted, and not restored by `fx resume`. A grant you gave in context stays in that context.

And the biggest one:

> **Project `.fx.json` files cannot define permission rules or set the model.** Permission policy lives only in the user's own settings. This is deliberate anti-repo-hijack design: a cloned repository cannot widen its own authority or swap the model out from under you.

Several harnesses get this wrong in one direction or another — repo-level settings that grant, or escape hatches that trust the checkout's config wholesale. fx drew the line in the right place on day one.

## auto mode: a model in the approval loop

`auto` is the default, and it puts a model where the human prompt was — the same move as [Claude Code's auto mode](/blog/claude-code-auto-mode-wrote-a-policy). The mechanics, precisely:

1. Rules are checked first. A matching rule resolves the call.
2. An unresolved sensitive call goes to an **automatic review by a hardcoded model request**. Which model? [The docs](https://fx.sh/docs/configure-fx/permissions.md) say `openai/gpt-5.4`; the launch-day source tree hardcodes `zai/glm-5.2` (`src/core/permissions/auto_classifier.zig`). Either way, no setting, env var, or model choice changes it.
3. The reviewer can only **allow or escalate** — an unresolved review falls through to a human prompt in interactive mode; it cannot silently hard-deny.
4. A transient reviewer failure triggers one bounded retry.

Two consequences to know before relying on it:

- **Each review is an extra model request.** `auto` can cost more than `ask` for the same call — the mode that removes the prompt adds inference.
- **The reviewer's decisions leave no documented record.** Nothing states what the reviewer was shown, what it decided, or why. A model is deciding what runs on your machine, and the only artifact is the session itself.

## Sensitive tools and the inspection grammar

The docs name the sensitive tools: `write_file`, `edit_file`, `delete_file`, `rename_file`, `copy_file`, `create_folder`, `run_command`, `open_file`, `install_skill`, `vision`.

In `ask` mode, fx carves out a **prompt-free inspection grammar**: restricted forms of `pwd`, `ls`, `wc`, and `printf`, and pipelines of up to 8 stages composed *only* of those. Everything else needs approval. Output from the grammar is escaped and capped at 65,536 bytes.

This is the deterministic answer to the "safe commands" problem, and it's the honest one: a parser decides what's prompt-free, not a model classifying its own command as harmless. The grammar is narrow — read-only inspection, nothing that writes or reaches the network — which is exactly why it can be prompt-free.

## The sandbox

Sandboxing is a **separate setting from permissions**, with three values:

- **`os`** — native sandbox, **macOS only**: reads allowed, writes restricted to the workspace, additional configured dirs, and temp; outbound network allowed; listening on localhost needs separate authority.
- **`none`** — **the default.**
- **`auto`**.

One more decision worth crediting: **widening sandbox access is a separate approval from approving the command.** Saying yes to a command is not saying yes to it escaping the write boundary — the two authorities are distinct prompts.

The caveats are the mirror image: the sandbox is off by default, macOS-only when on, and outbound network is open even inside it.

## Where the native model ends

fx's native controls are genuinely good for a day-one release: a clear mode model, a deterministic rules grammar with sane precedence, exact-match rule capture, ephemeral session grants, and the repo-config lockdown. Where the model ends:

- **No hook or plugin surface — externally.** The source contains an internal hook harness with a `pre_tool_use` event that can block or rewrite a call, but nothing attaches to it from outside: no settings key, no subprocess execution, no docs. It's where a native integration *would* go; today it's compiled-in only.
- **The reviewer is fixed and opaque.** Hardcoded, no setting to change it, no documented record of its decisions — and the docs and source disagree about which model it even is (`openai/gpt-5.4` in the docs, `zai/glm-5.2` in the launch-day tree). A model decides what runs on your machine, and you can't select it or audit it.
- **No documented record beyond the session.** No append-only log, no decision trail. Session grants evaporating is right for safety and also means there's no ledger of what was granted.
- **No cross-machine policy.** Rules live in each user's `~/.fx/settings.json`; two engineers on one codebase share nothing.
- **The real external seam is `fx acp`** — an Agent Client Protocol server (Zed's editor-embedding protocol, [unrelated to the Agentic Control Plane despite the initials](/acp-vs-acp)). Reading the source, this is more than a transport: fx sends every approval-gated call to the connected client as `session/request_permission` with the full tool arguments, honors the client's allow/deny, and resolves **any** error, cancellation, or malformed answer to deny. A client that can answer that request can stand on fx's call path. (A gateway override, `FX_GATEWAY_BASE_URL`, also exists in source — loopback-only, the local-inference mechanism — so a co-resident model proxy fits too.)

## Governing fx with ACP

The seam described above is exactly where [`@agenticcontrolplane/fx`](https://www.npmjs.com/package/@agenticcontrolplane/fx) stands. fx has no plugin API, so it isn't a plugin — it's a **protocol middleware**: a zero-dependency stdio proxy between your Agent Client Protocol client (Zed, or any ACP driver) and the `fx acp` subprocess. Every message passes through untouched except `session/request_permission` — the request fx sends for each approval-gated call, raw arguments attached. The middleware checks that call against your workspace policy first: a policy **deny** answers reject before the prompt ever reaches you, a policy **allow** answers allow-once on your behalf — never allow-always, so every subsequent call for the same tool still goes through governance instead of fx caching a blanket approval — an **ask** relays the original prompt to your client for a human decision, and every decision lands in the audit stream.

```sh
npm install -g @agenticcontrolplane/fx
acp-fx -- fx acp        # point your ACP client here instead of `fx acp`
```

Two details worth knowing, both from fx's own design: the middleware strips `FX_PERMISSION_MODE=yolo` from the child environment (the env bypass would otherwise silence the permission stream it governs), and in `--unattended` mode it fails closed — no reachable gateway means reject, matching fx's own posture on malformed answers. Interactively it fails open and loud: no key or no gateway, and it tells you on stderr and steps aside rather than bricking the session. [Source](https://github.com/agentic-control-plane/fx-acp-plugin).

What the layer adds is the same list the rest of [the comparison](/controls) converges on: rules that exist across harnesses (a deny you wrote for Claude Code now holds here too), a queryable decision trail where fx's model reviewer has none, and policy that is workspace-wide instead of per-user, per-machine. If you run fx without it, the recommendation stays fx's own: write the deny rules, leave `yolo` alone, and know that the record of what ran is the session and nothing else.

## Frequently asked questions


