Give AI agents secure access to your accounts - without sharing your credentials.
The Problem
AI agents need to access secured accounts on your behalf - manage your email, update a todo, publish something online. But how do you give them access? Today's approach is not ideal: agents are entrusted with access to API credentials - even MCPs don't keep them out of reach.
The Solution
Gap lets you give agents (like Claude Code, Codex, OpenClaw) access to your accounts without giving them your credentials.
Agents can only access secure APIs by going via Gap. You give them a Gap token - not your API keys. When they make a request to an API you've gapped, Gap injects your credentials for them. The agent never sees the credentials.
Why this matters:
- Prompt injection can't leak credentials - The agent doesn't have them. A malicious prompt can't trick the agent into revealing what it doesn't possess.
- Stolen tokens are useless off-machine - Prompt injection could exfiltrate a Gap token, but by default the proxy is only accessible on your machine (localhost loopback).
- Credentials are never exposed to agents - They're encrypted using the keychain (macOS) or under a dedicated service user (Linux); inaccessible to your agents.
- Gap can only be managed with your passcode - To manage gap, you must know the secret you set it up with. If you don't write this down on your machine, agents can't change anything.
- One-way credential flow - Credentials go into Gap and never come back out. There's no API to retrieve them, no export function. The only path out is privilege escalation on your machine.
- Monitor your agents - By funneling access through Gap, you can monitor and record all of their interactions with your secured services.
- Enforce policies - Gap can act as an enforcer of policies. Rate limit requests, restrict certain activities, require human authorisation, etc.
- Works with any agent or software stack - If it can use an HTTP proxy, it works with Gap. MCPs, cli tools, scripts in a skill - all of these can be easily gapped.
Get Started
macOS Quick Start
Download the native app (recommended):
- Download
Gap.dmgfrom the latest GitHub release - Open the DMG and drag Gap to Applications
- Launch Gap from Applications
Follow the instructions to set a password for managing your gap. Don't store this anywhere an agent could get at it (eg. a notes app), ideally just remember it.
Install a gap plugin for a given service (eg. Exa) and set credentials:
Set your API key:
Create a Gap token:
cd /path/to/your/project echo "GAP_TOKEN=gap_xxxxxxxxxxxx" >> .env
Use gapped MCPs, CLIs, etc. like this gapped fork of exa-mcp-server:
claude mcp add exa -- npx -y exa-gapped-mcp
The agent can now talk to Exa without direct API credentials - Gap injects them automatically.
Linux Quick Start
1. Download and install binaries
# Download the latest release (adjust version and arch as needed)
curl -LO https://github.com/mikekelly/gap/releases/latest/download/gap-linux-amd64.tar.gz
tar -xzf gap-linux-amd64.tar.gz
sudo mv gap gap-server /usr/local/bin/2. Create a dedicated user and directories
# Create gap user (no login shell, no home directory) sudo useradd --system --no-create-home --shell /usr/sbin/nologin gap # Create data directory with restricted permissions sudo mkdir -p /var/lib/gap sudo chown gap:gap /var/lib/gap sudo chmod 700 /var/lib/gap
3. Create systemd service
sudo tee /etc/systemd/system/gap-server.service > /dev/null <<EOF [Unit] Description=Gated Agent Proxy After=network.target [Service] Type=simple User=gap Group=gap Environment=GAP_DATA_DIR=/var/lib/gap ExecStart=/usr/local/bin/gap-server Restart=on-failure RestartSec=5 # Security hardening NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes ReadWritePaths=/var/lib/gap [Install] WantedBy=multi-user.target EOF
4. Start the service
sudo systemctl daemon-reload sudo systemctl enable gap-server sudo systemctl start gap-server # Check status sudo systemctl status gap-server
5. Initialize and configure
# Initialize with a password gap init # Install a plugin gap install mikekelly/exa-gap # Set your API key gap set mikekelly/exa-gap:apiKey # Create a token for your agent gap token create my-agent
Docker (for containerized agents)
Security note: The Docker deployment is designed for environments where agents also run in containers. If your agent runs directly on the host machine, use the native macOS/Linux installation instead - a host-based agent could potentially access the Docker volume and read credentials directly, bypassing the proxy's protection.
The Docker image is ideal for:
- Sandboxed agent environments (agent and Gap both containerized)
- Kubernetes deployments
- CI/CD pipelines with ephemeral agents
Quick start
GAP_ENCRYPTION_KEY is required for database encryption at rest. Generate it once and store it securely — losing it means losing access to your stored secrets.
# Generate an encryption key (do this once, save it securely) export GAP_ENCRYPTION_KEY=$(openssl rand -hex 32) # Run with persistent storage (required) docker run -d \ --name gap-server \ -v gap-data:/var/lib/gap \ -e GAP_ENCRYPTION_KEY \ -p 9443:9443 \ -p 9080:9080 \ mikekelly321/gap:latest
Example Docker Compose
services: gap-server: image: mikekelly321/gap:latest environment: - GAP_ENCRYPTION_KEY=${GAP_ENCRYPTION_KEY} volumes: - gap-data:/var/lib/gap # Export CA cert so agents can trust it - ./gap-ca.crt:/var/lib/gap/ca-export.crt:ro ports: - "9443:9443" - "9080:9080" networks: - agent-network my-agent: image: your-agent-image environment: # Proxy uses HTTPS, not HTTP - HTTPS_PROXY=https://gap-server:9443 # Agent needs to trust Gap's CA for both proxy and MITM - NODE_EXTRA_CA_CERTS=/certs/ca.crt - GAP_TOKEN=${GAP_TOKEN} volumes: # Mount CA cert from host - ./gap-ca.crt:/certs/ca.crt:ro networks: - agent-network depends_on: - gap-server volumes: gap-data: networks: agent-network:
Volume requirement
The container requires a volume mount for /var/lib/gap. Without it, secrets would be lost when the container stops:
# This will fail with a helpful error docker run mikekelly321/gap:latest # For testing only, you can bypass with: docker run -e GAP_ALLOW_EPHEMERAL=I-understand-secrets-will-be-lost mikekelly321/gap:latest
Build from Source
1. Build and start the server
git clone https://github.com/mikekelly/gap.git cd gap cargo build --release # Start the server ./target/release/gap-server &
2. Initialize and install a plugin
# Initialize with a password (you'll need this for admin operations) ./target/release/gap init # Install the Exa search plugin ./target/release/gap install mikekelly/exa-gap # Set your Exa API key ./target/release/gap set "mikekelly/exa-gap:apiKey"
How It Works
- Agent makes request through the proxy with its bearer token
- Gap authenticates the agent and checks which plugins it can use
- Plugin matches the target hostname (e.g.,
api.exa.ai) - Credentials loaded from secure storage
- JavaScript transform injects credentials into the request
- Request forwarded to the actual API
Security Model
Credentials are write-only and stored outside your user context:
- macOS: Stored encrypted using a key in the Keychain, isolated from user-space processes
- Linux: Stored with restricted permissions under a dedicated service user
There's no "get credential" API, no way to list credential values, no export function. The only way to use a credential is through the proxy - and the only way to extract one is privilege escalation to root/admin.
This is a fundamentally different security posture than giving credentials to an agent, where a single prompt injection could exfiltrate them to an attacker-controlled server.
Agent tokens: Tokens are for tracking and audit, not strong authentication. Any process that can read the token (other agents, scripts, humans with shell access) can use it. The real security boundary is the credential store - tokens just help you try and keep track of which agent made which request.
Plugins are simple JavaScript:
export default { name: "exa-gap", match: ["api.exa.ai"], credentialSchema: { fields: [ { name: "apiKey", label: "API Key", type: "password", required: true } ] }, transform(request, credentials) { request.headers["Authorization"] = `Bearer ${credentials.apiKey}`; return request; } };
Plugin Crypto APIs
The plugin runtime exposes cryptographic helpers under GAP.crypto and a base64 utility under GAP.util.
GAP.util.base64(input, decode?)
Encode or decode base64. Pass a string or Uint8Array to encode; pass true as the second argument to decode a base64 string to a Uint8Array.
var encoded = GAP.util.base64("hello"); // "aGVsbG8=" var decoded = GAP.util.base64("aGVsbG8=", true); // Uint8Array
GAP.crypto.sign(algorithm, keyDer, data)
Sign a message. keyDer must be a Uint8Array containing a PKCS#8 DER-encoded private key. Returns the raw signature bytes as a Uint8Array.
Supported algorithms: "ed25519", "ecdsa-p256", "rsa-pss-sha256", "rsa-pkcs1-sha256".
var keyDer = GAP.util.base64(credentials.privateKey, true); // decode from base64 var sig = GAP.crypto.sign("ed25519", keyDer, "message to sign");
GAP.crypto.verify(algorithm, publicKeyDer, signature, data)
Verify a signature. publicKeyDer is a Uint8Array of the DER-encoded public key; signature is the raw signature bytes as a Uint8Array. Returns true if the signature is valid, false otherwise (never throws on a bad signature).
var valid = GAP.crypto.verify("ed25519", publicKeyDer, sig, "message to sign");
GAP.crypto.httpSignature(options)
Signs an HTTP request following RFC 9421 HTTP Message Signatures. Returns an object with two header values ready to attach to the request:
signatureInput— the value for theSignature-Inputheadersignature— the value for theSignatureheader
Options:
| Field | Required | Description |
|---|---|---|
request |
yes | The request object passed to transform |
components |
yes | Array of components to sign, e.g. ["@method", "content-type"]. Derived components: @method, @target-uri, @authority, @path, @query. Any other string is treated as a header name. |
algorithm |
yes | Signing algorithm: "ed25519", "ecdsa-p256", "rsa-pss-sha256", or "rsa-pkcs1-sha256" |
keyId |
yes | Key identifier included in Signature-Input for the verifier |
keyDer |
yes | Uint8Array of PKCS#8 DER-encoded private key |
label |
no | Signature label (default: "sig1") |
created |
no | Unix timestamp in seconds (default: current time) |
Here is a complete plugin that signs every outgoing request with an Ed25519 key stored as a base64 credential:
export default { name: "my-signed-api", match: ["api.example.com"], credentialSchema: { fields: [ { name: "privateKey", label: "Private Key (base64 PKCS#8 DER)", type: "password", required: true }, { name: "keyId", label: "Key ID", type: "text", required: true } ] }, transform(request, credentials) { var keyDer = GAP.util.base64(credentials.privateKey, true); var result = GAP.crypto.httpSignature({ request: request, components: ["@method", "@path", "content-type"], algorithm: "ed25519", keyId: credentials.keyId, keyDer: keyDer }); request.headers["Signature-Input"] = result.signatureInput; request.headers["Signature"] = result.signature; return request; } };
The private key must be PKCS#8 DER format, base64-encoded. Generate one with OpenSSL:
# Generate an Ed25519 key and export as base64-encoded PKCS#8 DER openssl genpkey -algorithm ed25519 -outform DER | base64
Common Questions
Q: Can a malicious agent steal my credentials?
No. The agent never receives your credentials - they're injected at the network layer. Even a compromised agent can only make requests to APIs you've authorized, and you'll see the activity in your logs.
Q: What if someone steals the agent token?
Agent tokens control which APIs can be accessed, but not which credentials are used. A stolen token lets someone make requests on your behalf to authorized APIs - similar to API key theft, but scoped to specific services. You can revoke tokens instantly via gap token delete.
Q: How is this different from giving my API keys to the agent?
With Gap: credentials stay in secure storage, agent gets scoped access via token, you control which APIs, you can revoke instantly.
Q: Can I use this with Claude Code, Cursor, or other IDEs?
Yes, if the IDE supports HTTPS proxy configuration. The proxy uses HTTPS on port 9443 (not HTTP). Point it to https://localhost:9443 and configure the IDE to trust Gap's CA certificate at:
- macOS:
~/Library/Application Support/gap/ca.crt - Linux:
/var/lib/gap/ca.crt
Each IDE has different proxy settings - check their documentation.
Note: The proxy uses TLS 1.3 with post-quantum key exchange (X25519MLKEM768). macOS system curl (LibreSSL-based) may not be compatible; use an OpenSSL-based curl if needed.
Q: Do I need to trust the agent framework?
You need to trust it not to exfiltrate data it receives from APIs (like search results), but you don't need to trust it with your credentials. The agent never sees them.
Contributing
Contributions welcome! See the codebase structure:
gap-lib/- Core library (proxy, plugins, storage)gap-server/- Server daemongap/- CLI tool
Testing
Run every test suite (Rust, Go, and all Docker integration tests):
Skip the Docker tests for a fast local feedback loop:
bin/all-tests --no-docker
Available flags:
| Flag | Description |
|---|---|
--no-docker |
Skip all Docker tests; run Rust + Go only |
--rust-only |
Only run Rust unit/integration tests |
--go-only |
Only run Go unit tests (management-go) |
--docker-only |
Only run Docker integration tests |
Individual test suites:
# Rust unit + integration tests cargo test --workspace # Go unit tests cd management-go && go test ./... # Docker bash integration tests (password auth) docker compose --profile test up --build --abort-on-container-exit # Docker bash integration tests (request signing) docker compose --profile test-signing up --build --abort-on-container-exit # Docker Go integration tests (password auth) docker compose --profile go-test up --build --abort-on-container-exit # Docker Go integration tests (request signing) docker compose --profile go-test-signing up --build --abort-on-container-exit
License
MIT




