loading…
Search for a command to run...
loading…
Self-hosted, zero-knowledge encrypted, self-destructing secrets for secure agent-to-agent coordination
Self-hosted, zero-knowledge encrypted, self-destructing secrets for secure agent-to-agent coordination
Self-hosted zero-knowledge expiring secrets. Encrypt locally, store ciphertext on the server, share a URL with the decryption key in the fragment. The server never sees the plaintext or the key.
go install github.com/benderterminal/zkettle@latest
This installs to $GOPATH/bin (typically ~/go/bin). Make sure it's in your PATH: export PATH="$HOME/go/bin:$PATH"
curl -fsSL https://github.com/benderterminal/zkettle/releases/latest/download/zkettle-$(uname -s | tr A-Z a-z)-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -o /usr/local/bin/zkettle && chmod +x /usr/local/bin/zkettle
git clone https://github.com/benderterminal/zkettle.git && cd zkettle && make install
Paste this prompt into Claude Code, Cursor, or any MCP-compatible agent:
I want to set up zKettle — a self-hosted zero-knowledge secret sharing tool. Install it with go install github.com/benderterminal/zkettle@latest, then read the MCP setup instructions in the README at https://github.com/benderterminal/zkettle. When configuring the MCP server, use the absolute path to the installed binary (find it with which zkettle or check ~/go/bin/). Once configured, test the full workflow using the CLI: create a secret, read it back, and revoke it. Note that MCP servers are loaded at startup — the new tools won't be available until the next terminal session.
# Start the server with a Cloudflare tunnel (instant public URL)
zkettle serve --tunnel
# Or start locally
zkettle serve --port 3000
# Create a secret (in another terminal)
echo "my secret password" | zkettle create --views 1 --minutes 60
# → http://localhost:3000/s/abc123#key
# Read a secret
zkettle read "http://localhost:3000/s/abc123#key"
# → my secret password (use -c for clipboard, -o <file> for file output)
# Revoke a secret
zkettle revoke --server http://localhost:3000 --token <delete-token> <id>
Open the URL in a browser to reveal the secret via the web viewer.
# Build and run with Docker Compose
docker compose up -d
# Or build and run manually
docker build -t zkettle .
docker run -d -p 3000:3000 -v zkettle-data:/data zkettle
The container listens on port 3000 and stores data in /data. Configure with environment variables (see Configuration Reference).
zkettle serve --host 0.0.0.0 --tls-cert /path/to/cert.pem --tls-key /path/to/key.pem
Run zkettle behind Caddy, Nginx, or Traefik for automatic TLS:
zkettle serve --host 127.0.0.1 --trust-proxy
Enable --trust-proxy so zkettle reads the real client IP from X-Forwarded-For headers.
Copy the service template and enable it:
sudo cp contrib/zkettle.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now zkettle
The service template uses DynamicUser=yes with ReadWritePaths=/var/lib/zkettle, so systemd manages the data directory automatically.
Configure via environment file at /etc/zkettle/env:
ZKETTLE_PORT=3000
ZKETTLE_HOST=0.0.0.0
ZKETTLE_ADMIN_TOKEN=your-secret-token
ZKETTLE_TRUST_PROXY=true
The database is a single SQLite file at <data-dir>/zkettle.db. Back it up with:
sqlite3 /var/lib/zkettle/zkettle.db ".backup /backups/zkettle-$(date +%Y%m%d).db"
Enable the admin endpoint by setting an admin token via environment variable:
export ZKETTLE_ADMIN_TOKEN=my-secret-admin-token
zkettle serve
Note: Passing
--admin-tokenon the command line exposes the token in process listings (ps,/proc/*/cmdline). Prefer the environment variable or config file.
# Via CLI
zkettle list --server http://localhost:3000 --admin-token my-secret-admin-token
# Via API
curl -H "Authorization: Bearer my-secret-admin-token" http://localhost:3000/api/admin/secrets
Returns metadata only (ID, views remaining, timestamps). No encrypted content or decryption keys are ever exposed.
Returns 404 when no admin token is configured (endpoint disabled). Requires Authorization: Bearer <token> header.
Response (200):
[
{
"id": "abc123...",
"views_left": 2,
"expires_at": "2024-01-02T03:04:05Z",
"created_at": "2024-01-01T00:00:00Z"
}
]
Enable the /metrics endpoint with the --metrics flag:
export ZKETTLE_ADMIN_TOKEN=my-secret-admin-token
zkettle serve --metrics
The /metrics endpoint requires the admin token (Authorization: Bearer <token> header). Returns 404 when no admin token is configured.
Returns JSON metrics at GET /metrics:
{
"zkettle_secrets_active": 5
}
zKettle includes an MCP server for use with Claude Desktop, Claude Code, or any MCP-compatible agent.
Important: Use the absolute path to the
zkettlebinary. Many MCP clients do not inherit your shell's PATH, so a barezkettlecommand will silently fail to start.
Add to your MCP client's configuration file:
{
"mcpServers": {
"zkettle": {
"command": "/absolute/path/to/zkettle",
"args": ["mcp", "--port", "3001", "--tunnel"]
}
}
}
Use --tunnel for public shareable URLs via Cloudflare Quick Tunnel (no account required). Omit it for local-only access. Use --base-url https://your-domain.com if you have a custom domain.
Claude Code shortcut:
claude mcp add -s user zkettle -- /absolute/path/to/zkettle mcp --port 3001 --tunnel
Docker (for MCP verification/indexing):
docker build -t zkettle-mcp .
docker run --rm -i -p 3000:3000 -v zkettle-data:/data zkettle-mcp
The container defaults to zkettle mcp --host 0.0.0.0 --port 3000 --data /data.
The MCP server starts an HTTP backend on the specified port and communicates with the agent over stdio. All encryption and decryption happens locally — in the browser (Web Crypto API), CLI, or MCP server process. The zKettle HTTP server never sees plaintext.
Available tools:
| Tool | Description |
|---|---|
create_secret |
Encrypt and store a secret (content or file input), returns an expiring URL |
read_secret |
Retrieve and decrypt a secret (file or clipboard output to avoid context exposure) |
list_secrets |
List active secrets (metadata only) |
revoke_secret |
Delete a secret by ID |
generate_secret |
Generate a random secret, optionally store it (create=true) |
zkettle serve [options] Start the HTTP server
--port 3000 HTTP port
--host 127.0.0.1 Listen address (use 0.0.0.0 for all interfaces)
--data ./data Data directory for SQLite database
--base-url "" Base URL for generated links (default: http://localhost:{port})
--cors-origins "" Comma-separated allowed CORS origins
--tunnel Expose server via Cloudflare Quick Tunnel
--trust-proxy Trust X-Forwarded-For headers (behind a reverse proxy)
--log-format "" Log format: json or text (defaults to text)
--tls-cert "" TLS certificate file path
--tls-key "" TLS private key file path
--admin-token "" Admin API bearer token (enables GET /api/admin/secrets)
--max-secret-size 0 Max encrypted secret size in bytes (0 = 500KB)
--metrics Enable /metrics endpoint
zkettle create [options] Encrypt and store a secret (reads from stdin)
--views 1 Max views before auto-delete
--minutes 1440 Minutes until expiry (default 24h)
--server http://localhost:3000 Server URL
--json Output JSON to stdout
--quiet, -q Suppress stderr output
zkettle read [options] <url> Retrieve and decrypt a secret (quote the URL)
--clipboard, -c Copy to clipboard instead of printing to stdout
--file, -o <path> Write to file (0600 permissions) instead of stdout
zkettle revoke [options] <id> Delete a secret
--server http://localhost:3000 Server URL
--token "" Delete token (returned by create, or set ZKETTLE_DELETE_TOKEN)
zkettle list [options] List active secrets (requires admin token)
--server http://localhost:3000 Server URL
--admin-token "" Admin API bearer token
--json Output raw JSON
zkettle generate [options] Generate a cryptographically random secret
--length 32 Length in characters
--charset alphanumeric Character set: alphanumeric, symbols, hex, base64url
zkettle mcp [options] Start MCP server on stdio with HTTP backend
--port 3000 HTTP port for API
--host 127.0.0.1 Listen address
--data ./data Data directory
--base-url "" Base URL for generated links
--tunnel Expose server via Cloudflare Quick Tunnel
--trust-proxy Trust X-Forwarded-For headers (behind a reverse proxy)
--log-format "" Log format: json or text (defaults to text)
zkettle version Print version
Configuration is resolved in order of precedence: flags > env vars > config file > defaults.
Search order: ./zkettle.toml, $HOME/.config/zkettle/zkettle.toml
port = 3000
host = "127.0.0.1"
data = "./data"
base_url = ""
cors_origins = []
trust_proxy = false
tunnel = false
log_format = "" # defaults to "text"
tls_cert = ""
tls_key = ""
admin_token = ""
max_secret_size = 0 # 0 = 500KB default
metrics = false
Security: If your config file contains
admin_token, restrict permissions:chmod 600 zkettle.toml
| Variable | Description |
|---|---|
ZKETTLE_PORT |
HTTP port |
ZKETTLE_HOST |
Listen address |
ZKETTLE_DATA |
Data directory |
ZKETTLE_BASE_URL |
Base URL for generated links |
ZKETTLE_CORS_ORIGINS |
Comma-separated CORS origins |
ZKETTLE_TRUST_PROXY |
Trust proxy headers (true/1/yes) |
ZKETTLE_TUNNEL |
Enable Cloudflare tunnel (true/1/yes) |
ZKETTLE_LOG_FORMAT |
Log format: json or text |
ZKETTLE_TLS_CERT |
TLS certificate file path |
ZKETTLE_TLS_KEY |
TLS private key file path |
ZKETTLE_ADMIN_TOKEN |
Admin API bearer token |
ZKETTLE_MAX_SECRET_SIZE |
Max encrypted secret size in bytes |
ZKETTLE_METRICS |
Enable metrics endpoint (true/1/yes) |
ZKETTLE_DELETE_TOKEN |
Delete token for zkettle revoke (alternative to --token) |
Create a secret.
Request:
{
"encrypted": "<base64url ciphertext>",
"iv": "<base64url 12-byte IV>",
"views": 1,
"minutes": 1440
}
Constraints:
encrypted — required, base64url-encoded, max 500KB decoded (configurable via --max-secret-size)iv — required, base64url-encoded, must decode to exactly 12 bytesviews — 1-100 (default: 1)minutes — 1-43200 (default: 1440)Response (201):
{
"id": "abc123",
"expires_at": "2024-01-02T03:04:05Z",
"delete_token": "def456"
}
Errors: 400 (validation), 415 (wrong Content-Type), 429 (rate limited)
Retrieve and consume a view. Returns the encrypted blob:
{
"encrypted": "<base64url ciphertext>",
"iv": "<base64url IV>"
}
Errors: 400 (invalid ID format), 404 (expired, consumed, or nonexistent)
Check availability without consuming a view.
Response (200):
{
"status": "available"
}
Errors: 400 (invalid ID format), 404 (expired, consumed, or nonexistent)
Delete a secret. Requires Authorization: Bearer {delete_token} header.
Response: 204 No Content
Errors: 400 (invalid ID format), 401 (missing token), 404 (not found or wrong token)
Health check. Returns 200 with {"status":"ok"}, or 503 with {"status":"error"} if the database is unavailable.
Serves the web viewer HTML. The decryption key is in the URL fragment (#key) and never sent to the server.
ExtraRoutes and Middleware to extend the server with custom routes and middleware for any deployment.zkettle read) or MCP (read_secret), the decrypted plaintext appears in terminal output or the MCP tool result (which enters the AI agent's conversation context). To keep secrets out of terminal scrollback and agent logs, use --clipboard / --file (CLI) or the clipboard / file parameters (MCP). When creating secrets via MCP, use the file parameter to read content from a file, or generate_secret with create=true to generate and encrypt without exposing plaintext. In the web viewer, use the "Copy Without Revealing" button to copy a secret to clipboard without rendering it in the page.make build # Build for current platform
make build-all # Build for darwin/linux/windows amd64 + darwin/linux arm64
make install # Build and install to $GOPATH/bin or /usr/local/bin
make test # Run all tests
make clean # Remove build artifacts
AGPL-3.0 — see LICENSE.
Добавь это в claude_desktop_config.json и перезапусти Claude Desktop.
{
"mcpServers": {
"zkettle": {
"command": "npx",
"args": []
}
}
}