A tiny host-side credential-injection proxy for AI agents.
Not a gateway. Not a vault. Not a sandbox. Not a policy engine. Just a small reverse proxy that keeps your upstream API keys on the host and gives agents a lower-value, revocable proxy token instead.
It works with today's ugly reality: static API keys, bearer tokens, and arbitrary HTTP endpoints. No upstream changes required.
Most real APIs aren't going to adopt a new auth model just for agents. Your agent needs to call GitHub, Slack, Fireflies, or some internal service — and all of them hand you a static API key or bearer token.
The problem: if your agent has that key, it can exfiltrate it. Whether it's running in a sandbox, as a local coding assistant, or in a CI pipeline — if the agent can read an env var, a config file, or its own prompt, the key is exposed.
agent-airlock sits between the agent and the upstream API. The agent sends requests to the proxy with a scoped proxy token. The proxy validates the token, checks the request against an explicit allowlist, injects the real credential, and forwards it. The agent never sees the upstream key.
Agent agent-airlock Upstream API
│ │ │
│ POST /github/repos/o/r/issues │ │
│ Authorization: Bearer agent-tok │ │
│ ──────────────────────────────────→│ │
│ │ POST /repos/o/r/issues │
│ │ Authorization: Bearer ghp_real │
│ │ ──────────────────────────────────→│
│ │ │
│ │◀──────────────── 201 Created ──────│
│◀──────────────── 201 Created ──────│ │
There is growing activity in this space — MCP Secret Wrapper, Microsoft MCP Gateway, AgentGate, and others. The problem is real and people care about it.
agent-airlock is deliberately small and simple:
- ~300 lines of core logic, zero runtime dependencies — easy to audit, easy to understand
- Not MCP-specific — works with any HTTP API (REST, GraphQL, remote MCP, anything)
- Local-first — runs on your machine, talks over Unix socket or TCP, no cloud dependency
- Explicit allowlists — you declare exactly what's allowed, no hidden magic or implicit permissions
- No platform lock-in — a JSON config file and a Node.js process, nothing else
Install globally from npm:
npm install -g agent-airlockOr run directly with npx (zero install):
npx agent-airlock --config agent-airlock.jsonCreate a config file (see Configuration), then:
agent-airlock --config agent-airlock.jsongit clone https://github.com/es617/agent-airlock.git
cd agent-airlock
npm install
npm run build
npm start -- --config agent-airlock.json{
"listen": {
"unix": "/tmp/agent-airlock.sock"
},
"log": "/var/log/agent-airlock.log",
"tokens": [
{
"id": "my-agent",
"secret": "change-me-to-a-random-string",
"expires": "2026-04-01T00:00:00Z"
}
],
"upstreams": {
"github": {
"url": "https://api.github.com",
"auth": {
"header": "Authorization",
"value": "Bearer ghp_YOUR_TOKEN"
},
"allowlist": [
"GET /repos/**",
"POST /repos/*/issues",
"PATCH /repos/*/issues/*"
],
"denylist": [
"DELETE /**"
]
}
}
}See config.example.json for a full example with multiple upstreams.
| Field | Description |
|---|---|
unix |
Unix socket path (Windows: auto-converted to named pipe) |
host |
TCP bind address (default: 127.0.0.1) |
port |
TCP port |
Specify either unix or port.
Multiple tokens are supported — each agent gets its own token with independent expiry and revocation.
| Field | Description |
|---|---|
id |
Identifier shown in audit logs |
secret |
Bearer token value the agent uses |
expires |
Optional ISO 8601 expiry |
Generate tokens with:
agent-airlock --generate-token
# agk_a7f3b9c1e2d4f5a6b7c8d9e0f1a2b3c4...
agent-airlock --generate-token --id my-agent
# {
# "id": "my-agent",
# "secret": "agk_a7f3b9c1e2d4..."
# }Each upstream is a named route prefix. A request to /github/repos/foo is forwarded to https://api.github.com/repos/foo.
| Field | Description |
|---|---|
url |
Upstream base URL |
auth.header |
Header name to inject (e.g. Authorization) |
auth.value |
Header value to inject (e.g. Bearer ghp_xxx) |
allowlist |
Allowed METHOD /path patterns. Default-deny if empty. |
denylist |
Denied patterns. Checked before allowlist. |
graphql |
Optional body-aware filtering for GraphQL (see below) |
jsonrpc |
Optional body-aware filtering for JSON-RPC/MCP (see below) |
*matches a single path segment:GET /repos/*/issuesmatches/repos/foo/issues**matches zero or more segments:GET /repos/**matches/repos,/repos/foo,/repos/foo/bar/baz
For APIs where a single endpoint handles many operations (GraphQL, JSON-RPC/MCP), the proxy can inspect the request body to filter at the operation level. This is a coarse safety net, not fine-grained authorization — see Limitations.
{
"allowlist": ["POST /graphql"],
"graphql": {
"allow": ["query *", "mutation createItem", "mutation updateItem"],
"deny": ["mutation delete*"]
}
}query * allows any query. mutation delete* matches any mutation whose name starts with "delete". Operations not matching any allow rule are denied.
{
"allowlist": ["POST /mcp"],
"jsonrpc": {
"allow": ["tools/list", "tools/call:get_transcripts", "tools/call:search_*"],
"deny": ["tools/call:delete_*"]
}
}Rules use the format method:tool_name. tools/call:get_* allows any tool call whose name starts with "get_".
- Start agent-airlock on the host with a Unix socket:
{ "listen": { "unix": "/tmp/agent-airlock.sock" } }- Bind-mount the socket into the sandbox. A bind-mount makes a host file available inside the container. The agent inside the sandbox connects to the socket as if it were a local file — no network needed. In your OpenClaw gateway config, add the socket to the
bindsarray:
{
"agents": {
"defaults": {
"sandbox": {
"docker": {
"binds": [
"/tmp/agent-airlock.sock:/tmp/agent-airlock.sock:rw"
]
}
}
}
}
}This works even with "network": "none" — the sandbox stays fully network-isolated while the agent communicates with the proxy through the filesystem.
Important: After adding the bind-mount, restart the gateway (
openclaw gateway restart) so new containers pick up the change. The socket is automatically created with world-readable permissions (chmod 777) so containers running as non-root users can connect.
- Tell the agent to use the socket. In your skill/tool description:
To call the GitHub API, make HTTP requests to the Unix socket at /tmp/agent-airlock.sock.
Use the Authorization header with "Bearer <your-agent-token>".
Example: curl --unix-socket /tmp/agent-airlock.sock -H "Authorization: Bearer agent-token" http://localhost/github/repos/owner/repo
For agents running directly on the host (Claude Code, Cursor, etc.), use TCP or a Unix socket:
curl -H "Authorization: Bearer agent-token" \
http://localhost:3100/github/repos/owner/repoNote on HTTP security: All requests require a valid bearer token. However, plain HTTP on localhost means the token travels in cleartext. For stronger isolation, use a Unix socket — filesystem permissions ensure only your user can connect:
{ "listen": { "unix": "/tmp/agent-airlock.sock" } }
Errors generated by agent-airlock include an X-Agent-Airlock: error header and a "source": "agent-airlock" field, so agents can distinguish proxy errors from upstream errors:
{"error": "Forbidden: DELETE /repos/owner/repo", "source": "agent-airlock"}All requests are logged as JSON lines to stdout. Optionally also to a file:
{ "log": "/var/log/agent-airlock.log" }Each entry includes the token ID, so you can tell which agent made which request:
{"ts":"2026-03-09T12:00:00Z","token":"my-agent","method":"POST","upstream":"github","path":"/repos/owner/repo/issues","status":201,"ms":142}
{"ts":"2026-03-09T12:01:00Z","token":"ci-agent","method":"POST","upstream":"myapi","path":"/graphql","status":403,"ms":0,"graphql":"mutation deleteItem"}It narrows the blast radius without requiring upstreams to change.
- Credential isolation: Upstream API keys stay on the host. The agent only gets a revocable proxy token — lower value, independently rotatable, scoped to what you allow.
- Default-deny: Requests not matching an allowlist rule are rejected.
- Denylist priority: Deny rules are checked before allow rules.
- Token scoping: Each agent gets its own revocable token with optional expiry.
- No credential escalation: A compromised agent can't pivot to services it wasn't granted access to.
-
Confused deputy: A prompt-injected agent can still misuse any operation it has legitimate access to. This is inherent to giving agents tools with side effects — no proxy, MCP server, or tool framework solves this.
-
Proxy token theft: The agent does hold a proxy token. If compromised, that token can be exfiltrated and replayed. The damage is scoped and revocable, but not zero.
-
Host compromise: If an attacker reaches the host, they can read the config with upstream credentials. The proxy protects agents from credentials, not the host from attackers.
-
Bad allowlist design:
"POST /**"defeats the purpose. The proxy is only as good as your rules. -
GraphQL filtering limits: Operation-level filtering by name is a coarse safety net. GraphQL requests can be unnamed, aliased, use persisted queries, or bundle fields under one operation in ways that exceed intent. Not a substitute for authorization.
-
Upstream tool trust: JSON-RPC/MCP filtering relies on upstream naming conventions being stable and honest. If a remote server hides a destructive operation behind an innocuous tool name, the proxy can't catch it.
npm install
npm run dev -- --config config.example.json
npm testMIT — see LICENSE