# fx Permissions Reference — session/request_permission, Not Hooks

fx has no plugin or hook API. Its control seam is session/request_permission: acp-fx intercepts it, resolves allow/deny/ask, and scans display output.

<div style="background:var(--acp-surface,#f8f9fb);border:1px solid var(--acp-border,#e2e5ea);border-radius:12px;padding:20px 24px;margin:24px 0;">
<p style="margin:0 0 10px;font-weight:600;">Just want the answer?</p>
<p style="margin:0 0 12px;">fx has no hook or plugin API to install into. Point your Agent Client Protocol client at a proxy instead of running <code>fx acp</code> directly:</p>
<pre style="margin:0 0 12px;overflow-x:auto;"><code>npm install -g @agenticcontrolplane/fx
acp-fx -- fx acp</code></pre>
<p style="margin:0;">Native control model: <a href="/controls/fx">fx permissions, explained</a>. Not the product this site is named for: <a href="/acp-vs-acp">ACP vs. ACP</a>.</p>
</div>

fx is Vercel Labs' minimal coding agent, written in Zig and announced August 18, 2026 — and it comes with an acronym collision worth clearing up before anything else. fx speaks the **Agent Client Protocol**, Zed's editor-integration protocol, also abbreviated ACP. [It has nothing to do with the Agentic Control Plane this site is named for](/acp-vs-acp), beyond sharing three letters. This reference is about the real seam: the JSON-RPC message fx's protocol server sends for every approval-gated tool call, and the middleware that stands on it.

## No hook, no plugin API — externally

fx's source contains an internal hook harness with a `pre_tool_use` event that can block or rewrite a call before it runs. Nothing external attaches to it — no settings key, no subprocess execution, no documented interface. It's a compiled-in extension point today, not a surface a policy layer, plugin, or config file can register against.

## The real seam: `fx acp` and `session/request_permission`

fx can run as an Agent Client Protocol server (`fx acp`). Every approval-gated call fx wants to make gets sent to the connected client as a `session/request_permission` JSON-RPC request, carrying the full tool arguments. The client answers allow or deny, and fx honors it; any error, cancellation, or malformed answer resolves to **deny**. Only one such request is ever pending at a time — fx's own request/await cycle never has more than one in flight, so nothing on the receiving end needs correlation bookkeeping.

One wire detail worth knowing before writing anything against this: the original tool name never reaches the message. The `toolCall` payload carries a human-readable title (for example, "Run: rm -rf /tmp/scratch") and a coarse `kind` — read, edit, delete, move, execute, search, other — not fx's internal tool identifier. Any rule matching against this seam has to work on title substrings and kind, not an internal tool-name enum fx doesn't expose over the wire.

## `acp-fx`: a proxy, not a plugin

Because fx has no plugin API, the integration for it isn't one — it's **protocol middleware**: a zero-dependency stdio proxy sitting between a real Agent Client Protocol client (Zed, or any ACP driver) and the `fx acp` subprocess. Every message passes through untouched except `session/request_permission`, which the proxy intercepts, checks against `POST /govern/tool-use`, and resolves:

| Decision | Attended | `--unattended` |
|---|---|---|
| allow | reply `allow_once` directly | same |
| deny | reply `reject_once` directly | same |
| ask | forward the original request to the real client — a human decides | reply `reject_once` — an unattended agent can't self-approve |
| gateway unreachable / timeout | forward to the client; fail open with a loud `[ACP] ⚠ UNGOVERNED` line on stderr | reply `reject_once`; fail closed |
| no credential found | same as unreachable | same |

One deliberate constraint: the reply is always `allow_once` or `reject_once` — never `allow_always`, even when the policy check itself says allow. Every later call for the same tool goes through the same check again, instead of fx caching a blanket approval on the proxy's say-so.

## Display-layer output scanning — not a second enforcement point

fx's `session/update` notifications carry a tool's result text out to the client as it happens. `acp-fx` also posts that text to `POST /govern/tool-output` and can block or redact what actually reaches the client's display. State this plainly: it's **display-layer only**. fx has already sent the raw result to its own model before that notification goes out — blocking or redacting here changes what the human sees in their editor, not what the model already acted on. Treat it as a transcript filter, not a second gate on what the agent knows. An outage during this check passes the content through silently; the loud fail-open/fail-closed posture described above belongs to the pre-execution check, which is the one that actually controls whether something runs.

## Install and point your client at it

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

fx isn't among the harnesses the main installer wires up automatically — it doesn't appear in `install.sh` at all. Get it with its own README's command instead. It requires Node 22+ and a workspace credential in `~/.acp/credentials` (or `ACP_API_KEY`/`ACP_BEARER_TOKEN`).

## Failure posture, restated

The table above is the whole story: one bounded transport-failure retry (never a retry on an HTTP status code), then — attended — fail open, loud, with the connected client still getting the original request to decide on its own; unattended — `reject_once`, no exception, because there's nobody there to be the fallback.

## Known limitations

- fx's own `auto`-mode model reviewer — a hardcoded model that can approve a sensitive call when no rule matches it — is a separate, native fx feature. `acp-fx` only ever sees a call once fx has already decided it needs a permission check, so it doesn't observe or record what the reviewer approved upstream of that point.
- No model spend is metered here either: fx has no proxy path this middleware rides, so what lands in the activity log is the tool-call decision — `allow_once`/`reject_once`, title, kind, latency, reason — not per-call token cost.
- `FX_PERMISSION_MODE=yolo` bypasses fx's permission system entirely before any of the above runs. `acp-fx` strips that variable from the spawned child's environment so a stray shell export can't silently disable the checks it exists to make — but it can't stop `yolo` set some other way inside fx's own process.

## Frequently asked questions



## Where to read more

- [fx permissions, explained](/controls/fx)
- [ACP vs. ACP — the acronym collision](/acp-vs-acp)
- [Claude Code hooks reference](/blog/claude-code-hooks-reference)
- [Codex hooks reference](/blog/codex-cli-hooks-reference)
- [fx-acp-plugin source](https://github.com/agentic-control-plane/fx-acp-plugin)
- <!-- source: https://github.com/vercel-labs/fx --> [fx (Vercel Labs)](https://github.com/vercel-labs/fx)
- <!-- source: https://fx.sh/docs/configure-fx/permissions.md --> [fx permissions docs](https://fx.sh/docs/configure-fx/permissions.md)
