AgentPAM.ai

Deployment

How do I install this tomorrow

This is the most concrete page on the site. It does not answer “how does it work” — it answers which line you change, and how you verify it yourself afterwards.

Evidence for these config paths

Every config path on this page was verified on a real macOS machine in 2026-07 (with Claude Code and Codex CLI installed). Managed-policy file paths were confirmed to exist as concepts; the schema was not verified per vendor — see the open items at the foot of this page.

The mechanism

The core mechanism: command wrapping

We do not modify the agent, write a plugin, or install a kernel extension. We change the MCP server's launch command, so the agent starts our binary, and we start the real MCP server.

command-wrapping.txtscrolls horizontally
BEFORE
  agent  ──fork──>  MCP server        process holds GITHUB_PAT

AFTER
  agent  ──fork──>  agentpam  ──fork──>  MCP server   env has no secrets
                      
                      └──> control plane
                           attest / authorize / mint short-lived cred / audit
The mechanismWe did not invent this pattern. Teleport's tsh mcp connect and Keycard's keycard run use the same mechanism — it is the only insertion point for stdio that requires no code change from the customer.

Integration forms

Three integration forms — pick by what you already have

FormWhen to use itWhat you changeWhat we see
A. Command wrapping (local stdio)The agent talks directly to a local MCP server on a laptop — roughly 85% of real casesOne command line in the MCP configEvery tools/call + the outbound credential
B. We are the AS (remote HTTP + existing gateway)You already run an MCP GatewayOne line of gateway config (pointing at our AS)The authorization decision on every call
C. Local loopback proxyRemote MCP server, but you have no gatewayPoint the url in the MCP config at 127.0.0.1All traffic

A and B are the main paths and usually coexist — because real enterprises run both kinds of MCP server.

Per-agent config

Form A: exactly what changes, per agent

Claude Code

FileScopeCan the agent edit it?
~/.claude.jsonUser global (top-level mcpServers)⚠️ Yes
<project>/.mcp.jsonProject level⚠️ Yes
~/.claude/settings.jsonUser settings⚠️ Yes
/Library/Application Support/ClaudeCode/managed-settings.json (macOS)
/etc/claude-code/managed-settings.json (Linux)
Enterprise managed policyroot-owned; the agent cannot
Claude Code⚠️ On the machine we tested, the managed-policy file was not deployedthat is the default state. Enterprise deployment must push it via MDM, or the control can be edited away by the agent itself.
Before ~/.claude.json
{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx" }
    }
  }
}
After ~/.claude.json
{
  "mcpServers": {
    "github": {
      "command": "/usr/local/bin/agentpam",
      "args": ["run", "github", "--", "npx", "-y", "@modelcontextprotocol/server-github"]
    }
  }
}
The one visual detail that mattersThe env block disappears. The credential no longer exists in any config file, any process environment variable, or any location on disk. That single difference is the whole of Tier 0.

Codex CLI

Config lives at ~/.codex/config.toml (TOML, unlike Claude Code's JSON), with MCP servers under [mcp_servers.<name>].

Before ~/.codex/config.toml
[mcp_servers.github]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
env = { GITHUB_PERSONAL_ACCESS_TOKEN = "ghp_xxxxxxxxxxxx" }
After ~/.codex/config.toml
[mcp_servers.github]
command = "/usr/local/bin/agentpam"
args = ["run", "github", "--", "npx", "-y", "@modelcontextprotocol/server-github"]

Cursor / VS Code / others

Cursor

~/.cursor/mcp.json (user level) or <project>/.cursor/mcp.json (project level). Structurally identical JSON to Claude Code — the change is exactly the same.

VS Code / GitHub Copilot

<project>/.vscode/mcp.json or user settings; same shape. ⚠️ Note: VS Code deliberately puts secrets in inputs rather than plaintext in the config file — one of the few with a good default posture — but it still hands the credential to the MCP server process.

Cline / Goose / Gemini CLI / Windsurf

All are a command + args + env triple in JSON or YAML. The same wrapping logic covers all of them; only the file path and syntax differ.

Why one wrapper covers everythingMCP's stdio launch convention is uniform — every agent forks the child process from a command + args + env triple. So one wrapper covers every agent, and we do not need a per-agent adapter layer.

What it does

What agentpam run actually does

agentpam-run.txtscrolls horizontally
1. read own config: control-plane address, machine identity, this server's blueprint
2. ask the Local Attestor for device/process attestation  (Secure Enclave signature)
3. exchange human SSO assertion + agent instance assertion for Ts
       session token, DPoP-bound, 5-15 min
4. fork the real MCP server as a child process
       * scrub the environment explicitly: strip *_TOKEN / *_KEY / *_SECRET / AWS_* ...
       * the child receives no long-lived credential at all
5. relay JSON-RPC on stdin/stdout:
       · tools/list  ──> record tool descriptions, TOFU-pin them  (rug-pull detection)
       · tools/call  ──> send to the PDP for a decision
                         deny  ──> return an error; the request never reaches the server
                         allow ──> mint Tup (60-300s) and inject it outbound
6. write an audit event per call (including traceparent), push to the control plane
7. task termination / CAEP revocation event ──> invalidate the session immediately
Why it has to be a separate process, not a library
  1. It has to attest the calling process — a library runs inside the process being attested, so it would be attesting itself, which is void.
  2. A non-exportable Secure Enclave key requires its own code-signing identity.
  3. The credential must be held outside the agent process — that is what “never lands” means.

Form B

Form B: we act as the Authorization Server

If you already run an MCP Gateway, not a single line changes on the client. Everything happens on the gateway side.

gateway-config.jsoncscrolls horizontally
// 1) the gateway publishes protected resource metadata  (RFC 9728, mandatory)
//    GET /.well-known/oauth-protected-resource
{
  "resource": "https://mcp.customer.internal",
  "authorization_servers": ["https://agentpam.customer.internal"]
}

// 2) on 401 the gateway returns
//    WWW-Authenticate: Bearer resource_metadata=
//      "https://mcp.customer.internal/.well-known/oauth-protected-resource"

// 3) the gateway exchanges for the upstream credential  (agentgateway shown)
{
  "backendAuth": {
    "oauthTokenExchange": {
      "tokenEndpoint": "https://agentpam.customer.internal/token",
      "grantType": "urn:ietf:params:oauth:grant-type:token-exchange"
    }
  }
}
Form BRFC 9728 is a hard MUST and authorization_servers must name at least one AS. But the spec explicitly permits that AS to be the gateway itself (It may be hosted with the resource server or a separate entity.), so “any conformant gateway can point at us” is false — 4 of 12 tested support it cleanly. ⚠️ Step 2's WWW-Authenticate became optional in the 2025-11-25 spec (SEP-985).

Enterprise rollout

Rolling out at enterprise scale

Editing config on one machine is a POC. Enterprise deployment rests on three steps.

StepMechanismTooling
① Distribute the binarySigned pkg / deb / rpm, or a containerJamf · Intune · Ansible · Munki
② Push managed policyroot-owned managed settings that point every MCP server at the wrapperSame (MDM file push)
③ Lock itEnable “managed MCP config only”, forbid user-level overridesEach agent's enterprise policy switch
Step ② must be a root-owned fileThis is the trust premise of the entire approach: any control the agent can edit is not a control. Claude Code's own issue tracker documents the agent rewriting its own gate scripts (#32376, #61953). If the managed policy is user-writable, it is not a policy — it is a suggestion.
Additional requirements when air-gapped (C-13 / C-14 / C-15)
  • Binaries and container images travel on offline media (tarball + signature + checksum), imported with docker load.
  • The control plane sits on your internal network with zero outbound connections.
  • The wrapper only reaches the internal control plane address — never any service of ours.
  • Offline licence file, no online validation.

POC acceptance

The acceptance checklist you can run yourself

None of these seven needs us in the room. Run them, watch the result — this is how we want to be tested.

#CheckExpected result
1grep -rE "ghp_|sk-|AKIA" ~/.claude.json ~/.cursor ~/.codexNo hits (credentials removed from config)
2ps eww <mcp-server-pid> to inspect the child's environmentNo *_TOKEN / *_KEY at all
3Have the agent call a tool the policy deniesThe request never reaches upstream; the denial is in the audit log
4Have the agent exceed a parameter limit (e.g. refund $2500 against a $500 cap)Parameter-level denial
5Inspect one call in the upstream API's logsCarries a short-lived credential, attributable to a specific human + agent + task
6Dump the agent's process memory / transcript and search for the upstream credentialNot found
7Mark the task complete, then have the agent call againImmediately denied — even though the token has not expired
POC acceptanceItem 6 is how the core claim gets proven. Our MVP demo does exactly this: diff the agent transcript against the Authorization header the upstream actually received.

Known limitations

Known limitations

Listed honestly. These are not a backlog — they are the boundary of this approach.

LimitationDetailMitigation
A user can bypass itA developer can just run export GITHUB_TOKEN=… && curl in their own shellTier 2 egress backstop / Tier 3 OS-level enforcement (V2); but fundamentally this is insider risk, and outside this product's threat model
The config can be revertedWithout a root-owned managed policy, a user or the agent can put the old config backEnterprise deployment must use MDM + managed policy. That is a hard requirement, not an option
First registration needs to reach the control planeReachable on an internal network is enough, but it cannot be fully offlineInternal control plane
It adds a hop of latencyEvery tools/call costs one extra decision round tripTarget P99 < 200ms — ⚠️ not yet measured (Gap G-05)
It does not cover non-MCP pathsAn agent calling an SDK, psql, or kubectl directly does not pass through usTier 0 covers most of this in principle (the credential was never in the agent's hands); residual credentials fall to Tier 2 / 3

Still to verify

IDItemStatus
G-04Per-gateway support matrix for Form BOnly agentgateway verified
G-05End-to-end P99 latencyNot measured
G-15Exact managed-policy path and schema per agentPath existence verified; schema not verified — needs per-vendor documentation review
G-16Completeness of environment-variable scrubbingNeeds a blacklist + whitelist dual strategy and escape testing