Version: 0.3.3 Status: Implementation Complete
ClawGate is a secure bridge enabling isolated AI agents to access files and run git commands on a user's primary machine through capability-based, auditable access control. The system uses Ed25519-signed JWT tokens for fine-grained permissions and X25519/XChaCha20-Poly1305 end-to-end encryption over direct TCP connections.
Key properties:
- Capability-based access control with scoped, time-limited tokens
- End-to-end encryption with forward secrecy
- Outbound-only connections from the trusted machine
- Hardcoded forbidden paths for sensitive credentials
- Three-tier git permissions with command allowlists
- Custom tool proxy with path scoping and CWD confinement
- Audit trail of successful file, git, and tool operations on the resource daemon
This document is intended for developers, security auditors, and operators. It covers the internal architecture, security model, protocol specification, and deployment scenarios. For quick-start usage, see the README.
ClawGate consists of four main components:
-
Resource Daemon - Runs on the primary machine (your laptop/workstation). Connects outbound to the agent, validates tokens, executes file and git operations, and maintains the audit log.
-
Agent Daemon - Runs on the isolated machine (AI agent environment). Listens for connections, stores capability tokens, and proxies requests from local tools to the resource daemon.
-
MCP Server - Model Context Protocol server running on the isolated machine. Provides JSON-RPC 2.0 interface over stdio for AI tool integration.
-
CLI Tools - Commands for token management, file operations, key generation, and daemon control.
+-------------------------+ +-------------------------+
| PRIMARY MACHINE | | ISOLATED MACHINE |
| (Your Laptop) | | (AI Agent Host) |
|-------------------------| |-------------------------|
| | | |
| +------------------+ | E2E Encrypted | +------------------+ |
| | Resource Daemon |<--|-------- TCP -------|-->| Agent Daemon | |
| |------------------| | Port 53280 | |------------------| |
| | - Token verify | | | | - Token store | |
| | - File ops | | | | - IPC server | |
| | - Audit logging | | | +--------^---------+ |
| +------------------+ | | | |
| | | | Unix Socket |
| v | | | |
| +------------------+ | | +--------+---------+ |
| | File System | | | | CLI | |
| +------------------+ | | | (clawgate cat, | |
| | | | ls, write, ...) | |
| +------------------+ | | +--------^---------+ |
| | Ed25519 Keys | | | | |
| | ~/.clawgate/keys | | | subprocess |
| +------------------+ | | | |
| | | +--------+---------+ |
+-------------------------+ | | AI Tool | |
| | (OpenClaw) | |
| +------------------+ |
+-------------------------+
Request Path:
AI Tool (OpenClaw)
|
| subprocess call
v
CLI (clawgate cat, ls, ...)
|
| JSON (Unix socket IPC)
v
Agent Daemon
|
| Encrypted JSON (TCP)
v
Resource Daemon
|
| Token validation, scope check
v
File System
Response Path: Reverse of request path, with file content returned to stdout.
+-------------------------+ +-------------------------+
| PRIMARY MACHINE | | ISOLATED MACHINE |
| (Your Laptop) | | (AI Agent Host) |
|-------------------------| |-------------------------|
| | | |
| +------------------+ | E2E Encrypted | +------------------+ |
| | Resource Daemon |<--|-------- TCP -------|-->| Agent Daemon | |
| |------------------| | Port 53280 | |------------------| |
| | - Token verify | | | | - Token store | |
| | - File ops | | | | - IPC server | |
| | - Audit logging | | | +--------^---------+ |
| +------------------+ | | | |
| | | | Unix Socket |
| v | | | |
| +------------------+ | | +--------+---------+ |
| | File System | | | | MCP Server | |
| +------------------+ | | |------------------| |
| | | | - JSON-RPC 2.0 | |
| +------------------+ | | | - stdio | |
| | Ed25519 Keys | | | +--------^---------+ |
| | ~/.clawgate/keys | | | | |
| +------------------+ | | stdio |
| | | | |
+-------------------------+ | +--------+---------+ |
| | AI Tool | |
| | (Claude, etc.) | |
| +------------------+ |
+-------------------------+
Request Path:
AI Tool (Claude)
|
| JSON-RPC (stdio)
v
MCP Server
|
| JSON (Unix socket IPC)
v
Agent Daemon
|
| Encrypted JSON (TCP)
v
Resource Daemon
|
| Token validation, scope check
v
File System
Response Path: Reverse of request path, with file content base64-encoded.
The connection model is designed for security:
-
Resource daemon initiates - The trusted machine always connects outbound. No inbound connections to your laptop are required.
-
Agent daemon listens - The isolated machine accepts connections on port 53280 (configurable).
-
Single active connection - One resource daemon connects to one agent daemon at a time.
-
Persistent connection - The connection remains open for the session duration, with automatic reconnection on disconnect.
ClawGate assumes the following threat model:
Trusted:
- The primary machine running the resource daemon
- The user's Ed25519 signing keys
- The local file system on the primary machine
Untrusted:
- The isolated machine running the AI agent
- The network between machines
- The AI agent itself
- Any process on the isolated machine
Threats Addressed:
- Network eavesdropping (E2E encryption)
- Token forgery (Ed25519 signatures)
- Token replay (nonce-based encryption, expiration)
- Path traversal attacks (canonicalization)
- Unauthorized file access (capability scopes)
- Credential theft (hardcoded forbidden paths)
- Session hijacking (forward secrecy)
All communication is encrypted end-to-end using:
| Component | Algorithm | Purpose |
|---|---|---|
| Key Exchange | X25519 ECDH | Establish shared secret |
| Encryption | XChaCha20-Poly1305 | Authenticated encryption |
| Key Derivation | HKDF-SHA256 | Derive session key |
Forward Secrecy: Fresh X25519 keypairs are generated per session. Past sessions cannot be decrypted even if long-term keys are compromised.
Replay Prevention: Counter-based nonces ensure each message is unique within a session. Cross-session replay fails due to different session keys.
Access is controlled by signed capability tokens:
| Property | Value |
|---|---|
| Signature | Ed25519 (EdDSA) |
| Key Size | 32-byte public, 64-byte secret |
| Signature Size | 64 bytes |
Tokens are:
- Scoped - Limited to specific paths via glob patterns
- Time-limited - Expire after configurable TTL
- Operation-limited - Specify allowed operations (read/write/list/stat)
- Signed - Cannot be forged without the secret key
Multiple layers protect against path-based attacks:
-
Canonicalization - Paths are normalized to remove
..,., and//before any checks. Attempts to escape via traversal are rejected. -
Symlink Rejection - All file operations reject symbolic links, preventing symlink-based scope escapes.
-
Forbidden Paths - Hardcoded patterns block access to sensitive locations regardless of token scope.
-
Tool Path Scanning - For custom tools, all non-flag arguments are scanned for path-like patterns (
/...,~/...,./...,../...). Detected paths are canonicalized and validated against the tool's scope and forbidden path list. Tools without a scope block all path arguments. -
CWD Confinement - Tool subprocesses execute with their working directory set to
$HOME, ensuring relative paths resolve from a known, predictable base rather than the daemon's working directory.
Important: Denied operations fail immediately on the agent side (with error messages like "No token grants access") and never reach the resource daemon. This is a security feature - unauthorized requests are rejected before crossing the network.
Operations that reach the resource daemon are logged persistently to
~/.clawgate/logs/audit.log with:
- ISO 8601 UTC timestamp
- Request ID for tracing
- Operation type (read/write/list/stat/git)
- Target path
- Success/failure status
- Error code on failure (e.g., SCOPE_VIOLATION, INVALID_TOKEN)
Events are also emitted to stderr via std.log.info with AUDIT: prefix.
A daemon_start entry is written when the resource daemon starts.
Log format:
<timestamp> AUDIT req=<id> op=<op> path=<path> success=<bool> [error=<code>]
| Primitive | Algorithm | Key/Output Size | Standard |
|---|---|---|---|
| Token Signing | Ed25519 | 32B pub / 64B sec / 64B sig | RFC 8032 |
| Key Exchange | X25519 | 32B keys / 32B shared | RFC 7748 |
| Encryption | XChaCha20-Poly1305 | 32B key / 24B nonce / 16B tag | RFC 8439 ext. |
| Key Derivation | HKDF-SHA256 | Variable | RFC 5869 |
Key Generation:
clawgate keygen -o ~/.clawgate/keysCreates:
secret.key- 64-byte Ed25519 secret key (permissions: 0600)public.key- 32-byte Ed25519 public key (permissions: 0644)
Secret Zeroing: All cryptographic secrets are explicitly zeroed using
std.crypto.secureZero() when no longer needed, preventing recovery from
freed memory.
The following paths are always blocked, regardless of token scope:
Directory Patterns (substring match):
/.ssh/ SSH keys and configuration
/.gnupg/ GPG keys and keyrings
/.clawgate/keys/ ClawGate's own signing keys
/.aws/ AWS credentials
/.config/gcloud/ Google Cloud SDK credentials
/.azure/ Azure credentials
/.kube/ Kubernetes configuration
/.docker/config.json Docker credentials
/.netrc Network credentials
/.npmrc NPM authentication tokens
/.git-credentials Git credential storage
/.password-store/ Password manager data
/.local/share/keyrings/ System keyrings
/.mozilla/firefox/ Firefox profiles (passwords, cookies)
/.config/google-chrome/ Chrome profiles
/.config/chromium/ Chromium profiles
/.config/Code/ VS Code secrets
/.config/op/ 1Password CLI
File Suffixes (exact match):
.env Environment secrets
.env.local Local environment overrides
.env.production Production secrets
/private.pem Private keys
/private.key Private keys
/id_rsa SSH private key
/id_ed25519 SSH private key
/id_ecdsa SSH private key
.p12 Certificate bundles
.pfx Certificate bundles
credentials.json Service credentials
service-account.json GCP service accounts
secrets.json Application secrets
secrets.yaml Application secrets
secrets.yml Application secrets
Special Pattern:
- Any path containing
/.env(hidden .env files anywhere)
These patterns cannot be overridden by configuration or tokens.
Tokens use the standard JWT format:
BASE64URL(header).BASE64URL(payload).BASE64URL(signature)
{
"alg": "EdDSA",
"typ": "JWT"
}Only EdDSA (Ed25519) algorithm is accepted. Any other algorithm is rejected.
{
"iss": "clawgate:resource:laptop",
"sub": "clawgate:agent:minipc",
"iat": 1706745600,
"exp": 1706832000,
"jti": "cg_a1b2c3d4e5f6g7h8i9j0k1l2",
"cg": {
"v": 1,
"cap": [
{
"r": "files",
"o": ["read", "list", "stat"],
"s": "/home/mario/projects/**"
}
]
}
}| Claim | Type | Description |
|---|---|---|
iss |
string | Issuer identity (resource daemon) |
sub |
string | Subject identity (agent daemon) |
iat |
i64 | Issued-at Unix timestamp |
exp |
i64 | Expiration Unix timestamp |
jti |
string | Unique token ID (cg_ + 24 hex chars) |
cg.v |
u8 | ClawGate claims version (currently 1) |
cg.cap |
array | Array of capability grants |
cg.cap[].r |
string | Resource type (always "files") |
cg.cap[].o |
array | Operations: read, write, list, stat, git, git_write, git_remote |
cg.cap[].s |
string | Scope pattern (glob) |
| Pattern | Example | Matches |
|---|---|---|
| Exact | /home/mario/file.txt |
Only that exact file |
| Single-level | /tmp/* |
Direct children of /tmp |
| Recursive | /home/mario/** |
All descendants |
| Extension | /src/*.zig |
.zig files in /src (not recursive) |
Pattern Details:
- Paths must be absolute (start with
/) *matches any characters except/**matches any characters including/- Patterns are matched after path canonicalization
1. CREATION (Primary Machine)
clawgate grant --read --ttl 24h ~/projects > token.txt
|
v
2. TRANSFER (Manual)
Copy token.txt to isolated machine
Copy public.key to isolated machine
|
v
3. STORAGE (Isolated Machine)
clawgate token add "$(cat token.txt)"
Token stored in ~/.clawgate/tokens/
|
v
4. VALIDATION (On Each Request)
- Parse JWT structure
- Verify Ed25519 signature
- Check expiration
- Match scope pattern
- Check forbidden paths
|
v
5. EXPIRATION (Automatic)
Token becomes unusable after exp timestamp
Receive Request with Token
|
v
Parse JWT
(3 parts separated by '.')
|
[Parse Error?] --Yes--> Reject: INVALID_TOKEN
|
No
v
Verify Signature
(Ed25519 against public key)
|
[Invalid?] --Yes--> Reject: INVALID_TOKEN
|
No
v
Check Expiration
(now <= exp)
|
[Expired?] --Yes--> Reject: TOKEN_EXPIRED
|
No
v
Canonicalize Request Path
(resolve ., .., //)
|
[Escape Detected?] --Yes--> Reject: INVALID_PATH
|
No
v
Check Forbidden Paths
(hardcoded patterns)
|
[Forbidden?] --Yes--> Reject: ACCESS_DENIED
|
No
v
Match Token Scope
(glob pattern against path)
|
[No Match?] --Yes--> Reject: SCOPE_VIOLATION
|
No
v
Execute Operation
TCP with Length Prefix:
+----------------+------------------+
| Length (4B) | Payload |
| Big-endian | (variable) |
+----------------+------------------+
| Parameter | Value |
|---|---|
| Default Port | 53280 |
| Length Prefix | 4 bytes, big-endian |
| Max Message Size | 100 MB |
Phase 1: Key Exchange
Resource daemon connects and sends:
{
"version": 1,
"resource_pubkey": "<base64 X25519 ephemeral public key>",
"resource_id": "clawgate-resource"
}Agent daemon responds:
{
"ok": true,
"agent_pubkey": "<base64 X25519 ephemeral public key>",
"session_id": "sess_a1b2c3d4e5f6g7h8"
}Or on error:
{
"ok": false,
"error": "Version 2 not supported"
}Phase 2: Session Establishment
Both parties:
- Compute X25519 shared secret:
shared = X25519(my_secret, their_public) - Derive session key:
key = HKDF-SHA256(salt="clawgate-e2e-v1", ikm=shared, info=session_id) - Initialize nonce counter to 0
All subsequent messages are encrypted.
+-------------+-------------------+-----------+
| Nonce (24B) | Ciphertext | Tag (16B) |
+-------------+-------------------+-----------+
| Field | Size | Description |
|---|---|---|
| Nonce | 24 bytes | Counter-based (8B counter + 16B zeros) |
| Ciphertext | Variable | XChaCha20-encrypted payload |
| Tag | 16 bytes | Poly1305 authentication tag |
Overhead: 40 bytes per message (24 nonce + 16 tag)
{
"id": "req_12345",
"token": "<JWT capability token>",
"op": "read",
"params": {
"path": "/home/mario/file.txt",
"offset": 0,
"length": 4096
}
}| Field | Type | Description |
|---|---|---|
id |
string | Unique request ID for correlation |
token |
string | JWT capability token |
op |
string | Operation: read, write, list, stat, git, tool, tool_list |
params |
object | Operation-specific parameters |
Operation Parameters:
| Operation | Parameter | Type | Description |
|---|---|---|---|
| read | path | string | Absolute file path |
| read | offset | u64? | Starting byte offset |
| read | length | u64? | Max bytes to read |
| write | path | string | Absolute file path |
| write | content | string | Base64-encoded content |
| write | mode | string | create, overwrite, or append |
| list | path | string | Absolute directory path |
| list | depth | u32? | Listing depth (default: 1) |
| stat | path | string | Absolute path |
| git | path | string | Absolute repository path |
| git | args | string[] | Git arguments (e.g. ["status", "--short"]) |
| tool | tool_name | string | Registered tool name |
| tool | tool_args | string[]? | Arguments to pass to the tool |
| tool | input | string? | Base64-encoded stdin data |
| tool_list | (none) | Returns authorized tools |
Success:
{
"id": "req_12345",
"ok": true,
"result": { ... }
}Error:
{
"id": "req_12345",
"ok": false,
"error": {
"code": "SCOPE_VIOLATION",
"message": "Path not in granted scope"
}
}Result Types:
Read:
{
"content": "<base64-encoded bytes>",
"size": 1024,
"truncated": false
}Write:
{
"bytes_written": 256
}List:
{
"entries": [
{"name": "file.txt", "type": "file", "size": 1024},
{"name": "subdir", "type": "dir", "size": null}
]
}Stat:
{
"exists": true,
"type": "file",
"size": 4096,
"modified": "2026-01-31T10:00:00Z"
}Git:
{
"stdout": "M src/main.zig\n?? new_file.txt\n",
"stderr": "",
"exit_code": 0,
"truncated": false
}Tool:
{
"tool_name": "calc",
"stdout": "4\n",
"stderr": "",
"exit_code": 0,
"truncated": false
}Tool List:
{
"tools": [
{
"name": "calc",
"description": "Calculator (bc)",
"arg_mode": "allowlist",
"allow_args": ["-q"],
"examples": ["echo \"2+2\" | clawgate tool calc"]
}
]
}| Code | Description |
|---|---|
INVALID_TOKEN |
Token parse or signature failed |
TOKEN_EXPIRED |
Token has expired |
SCOPE_VIOLATION |
Path not within token's scope |
INVALID_OP |
Unknown operation |
INVALID_PATH |
Path canonicalization failed |
INVALID_REQUEST |
Malformed request JSON |
FILE_NOT_FOUND |
File or directory not found |
ACCESS_DENIED |
Permission denied or forbidden path |
FILE_TOO_LARGE |
File exceeds 100 MB limit |
NOT_A_FILE |
Expected file, got directory |
NOT_A_DIRECTORY |
Expected directory, got file |
IS_SYMLINK |
Symlinks not allowed |
GIT_ERROR |
Git command execution failed |
GIT_BLOCKED |
Git command or flag blocked by allowlist |
GIT_NOT_REPO |
Target path is not a git repository |
GIT_TIMEOUT |
Git command timed out |
TOKEN_REVOKED |
Token has been revoked |
TOOL_DENIED |
No tool registry or token lacks tool access |
TOOL_TIMEOUT |
Tool execution timed out |
TOOL_ERROR |
Tool execution failed |
ARG_BLOCKED |
Tool argument blocked by allow/deny list |
PATH_BLOCKED |
Tool path argument outside scope or forbidden |
INTERNAL_ERROR |
Unexpected server error |
ClawGate supports executing git commands on repositories hosted on the primary machine. Git operations use a three-tier permission model with command allowlists for defense in depth.
| Tier | Token Operation | Grants | Example Commands |
|---|---|---|---|
| Read-only | git |
Read-only git ops | status, diff, log, show, branch (list), blame |
| Write | git_write |
Mutating git ops | add, commit, checkout, merge, rebase, reset |
| Remote | git_remote |
Remote git ops | push, pull, fetch, remote add/remove |
Each tier implies the previous: git_remote > git_write > git.
clawgate grant --git ~/projects/myapp # git only (exact repo path)
clawgate grant --git ~/projects/** # git + file read/list/stat (recursive)
clawgate grant --git-write ~/projects/** # + git_write + file write
clawgate grant --git-full ~/projects/** # + git_remoteScope behavior: Git operations validate the repository root path against
the token scope. An exact path grant (no glob) is sufficient for git commands
alone. However, --git also enables file read/list/stat operations, which
validate individual file paths - these require a /** glob to access files
within the repository.
{
"id": "req_abc123",
"token": "<jwt>",
"op": "git",
"params": {
"path": "/home/mario/projects/myapp",
"args": ["diff", "--stat", "HEAD~3"]
}
}{
"id": "req_abc123",
"ok": true,
"result": {
"stdout": " src/main.zig | 5 ++---\n 1 file changed\n",
"stderr": "",
"exit_code": 0,
"truncated": false
}
}Output is truncated at 512 KB (same as file reads). The truncated flag
indicates when output was cut short.
status, diff, log, show, branch (list only), tag (list only),
rev-parse, ls-files, ls-tree, blame, shortlog, describe,
name-rev, rev-list, cat-file, diff-tree, diff-files, diff-index,
for-each-ref, symbolic-ref, stash list, remote (-v, show),
config --get/--get-all/--list (read-only config)
All of tier 1 plus:
add, commit, checkout, switch, merge, rebase, reset, stash
(save/pop/apply/drop), cherry-pick, revert, clean, rm, mv,
restore, branch (create/delete), tag (create/delete), am, apply,
format-patch, notes, config (set)
All of tier 1 + tier 2 plus:
push, pull, fetch, remote (add/remove/set-url), submodule, clone
These top-level git flags are always rejected (all tiers):
| Flag | Reason |
|---|---|
-c |
Arbitrary config override (e.g. core.fsmonitor) |
--exec-path |
Arbitrary executable path |
--git-dir |
Escape scope to different repository |
--work-tree |
Escape scope to different directory |
-C |
We set cwd ourselves; prevent confusion |
Per-subcommand blocks:
| Subcommand + Flag | Reason |
|---|---|
rebase --exec |
Runs arbitrary shell commands |
am --exec |
Runs arbitrary shell commands |
diff --ext-diff |
Runs external diff program |
config --global |
Modify system-wide config |
config --system |
Modify system-wide config |
filter-branch |
Always blocked (runs shell commands) |
Receive Git Request
|
v
Extract subcommand from args
|
v
Classify subcommand tier
(read / write / remote / blocked)
|
[Blocked?] --Yes--> Reject: GIT_BLOCKED
|
No
v
Check token has required tier
(git / git_write / git_remote)
|
[Insufficient?] --Yes--> Reject: ACCESS_DENIED
|
No
v
Validate args against blocked flags
|
[Blocked flag?] --Yes--> Reject: GIT_BLOCKED
|
No
v
Verify .git/ exists in repo path
|
[Not a repo?] --Yes--> Reject: GIT_NOT_REPO
|
No
v
Execute: git -C <repo_path> <args...>
|
v
Return GitResult (stdout, stderr, exit_code)
- Git runs on the resource daemon (trusted machine) using the user's own git config and credentials
- Command allowlists prevent arbitrary code execution via git hooks and exec flags
- Forbidden paths still apply - git can't access
~/.ssh/etc. - Scope validation - git only runs in token-scoped directories
- Output truncation at 512 KB prevents memory exhaustion
--git-dir/--work-treeblocked prevents pointing git at repos outside scope
ClawGate supports registering arbitrary command-line tools on the resource machine and invoking them remotely from the agent. Tools go through the same zero-trust pipeline as file and git operations: capability tokens, argument validation, path scoping, output truncation, audit logging, and end-to-end encryption.
Tools are registered on the resource machine via the CLI and stored in
~/.clawgate/tools.json. Each tool defines:
| Field | Description |
|---|---|
command |
The executable command (e.g. "bc -l", "rg") |
arg_mode |
allowlist (default) or passthrough |
allow_args |
Permitted flags (allowlist mode) |
deny_args |
Blocked flags (passthrough mode) |
scope |
Semicolon-separated paths relative to $HOME (or null) |
timeout_seconds |
Maximum execution time |
max_output_bytes |
Output truncation limit |
The scope field controls what filesystem paths a tool can access through
its arguments:
| Scope Value | Meaning |
|---|---|
null (no scope) |
Zero filesystem access. All path-like arguments blocked. |
"projects/webapp" |
Access to $HOME/projects/webapp and descendants |
"projects;docs" |
Access to both $HOME/projects and $HOME/docs |
Scope entries are always relative to $HOME. Semicolons separate multiple
entries. Each entry grants recursive access to that directory tree.
Rejected scope values:
.- equivalent to entire$HOME, too permissive..- escapes$HOME- Absolute paths (e.g.
/etc) - scopes are always$HOME-relative - Empty segments (e.g.
"a;;b") - Segments containing
..(e.g."projects/../etc")
Tool execution has three layers of argument security:
Tool Invocation Request
|
v
Layer 1: Flag Validation
(allowlist / denylist mode)
|
[Blocked flag?] --Yes--> Reject: ARG_BLOCKED
|
No
v
Layer 2: Path Scanning
For each non-flag argument:
- Is it path-like? (/..., ~/..., ./..., ../..., ., ..)
- No scope? Block any path-like arg -> PATH_BLOCKED
- Has scope? Expand ~, resolve relative paths against
$HOME, canonicalize, check forbidden paths,
check against scope entries via isWithin()
|
[Out of scope?] --Yes--> Reject: PATH_BLOCKED
[Forbidden?] ----Yes--> Reject: PATH_BLOCKED
|
No
v
Layer 3: CWD Confinement
Execute subprocess with cwd=$HOME
|
v
Return ToolResult (stdout, stderr, exit_code)
Allowlist mode (default): Only explicitly listed flags pass through.
Any flag not in allow_args is rejected with ARG_BLOCKED.
Passthrough mode: All flags pass through except those in deny_args.
The --flag=value form is also checked (e.g. --exec=evil is blocked
if --exec is denied).
Both modes always allow positional (non-flag) arguments to pass to Layer 2.
All non-flag arguments are scanned for syntactically unambiguous path forms:
| Pattern | Example | Detected? |
|---|---|---|
| Absolute | /etc/hosts |
Yes |
| Tilde | ~/project/file.zig |
Yes |
| Dot-relative | ./file, ../file |
Yes |
| Current/parent dir | ., .. |
Yes |
| Bare filename | hosts, Makefile |
No (could be subcommand) |
| Flag | -n, --verbose |
No (handled by Layer 1) |
For each detected path:
- Expand tilde to
$HOME(consistent withpath.expand()) - Resolve relative paths against
$HOME(the CWD base) - Canonicalize via
scope.canonicalizePath()- rejects..escapes, null bytes, non-absolute results - Check forbidden paths via
isForbiddenPath()-.ssh,.gnupg,.aws, etc. are blocked even if within scope - Check scope entries - the canonical path must fall within at least
one
$HOME/<scope_entry>viascope.isWithin()
If no scope entry matches, the path is rejected with PATH_BLOCKED.
All tool subprocesses execute with their working directory set to $HOME.
This provides defense in depth: even if a bare filename slips through path
detection (e.g. hosts), it resolves within $HOME rather than a
potentially sensitive directory.
{
"id": "req_abc123",
"token": "<jwt>",
"op": "tool",
"params": {
"tool_name": "rg",
"tool_args": ["pattern", "~/projects/webapp/src"],
"input": null
}
}{
"id": "req_abc123",
"ok": true,
"result": {
"tool_name": "rg",
"stdout": "src/main.zig:42: pattern match\n",
"stderr": "",
"exit_code": 0,
"truncated": false
}
}Agents can discover available tools via tool_list. This request can be
sent without a token (tokenless discovery) for metadata only, or with a
token for scope-filtered results.
Tokenless request (returns all registered tools):
{"op": "tool_list", "params": {}}Authenticated request (returns only tools the token can invoke):
{
"id": "req_abc123",
"token": "<jwt>",
"op": "tool_list",
"params": {}
}- Commands are split into argv tokens and executed via
std.process.runorstd.process.spawn(when stdin is provided) - never through a shell - Output is collected with a 2x buffer (to handle truncation gracefully)
and trimmed to
max_output_bytes - The
truncatedflag indicates when output was cut short - Exit code is captured from the child process (signals mapped to 128)
| Limitation | Description |
|---|---|
| Symlinks within scope | A symlink inside scope pointing outside cannot be caught by argument scanning. Future: Landlock sandboxing. |
| Paths via stdin | If a tool reads paths from stdin, they cannot be validated. CWD confinement mitigates but doesn't prevent absolute paths. |
| Flag values | --file=/etc/hosts - the flag is controlled by allow/deny lists, but the path value is not separately validated. |
| Environment variables | A tool could read $HOME or other env vars to construct paths. Future: subprocess environment sanitization. |
The MCP (Model Context Protocol) server enables AI tools to access ClawGate capabilities via JSON-RPC 2.0 over stdio.
| Method | Description |
|---|---|
initialize |
Returns server capabilities |
tools/list |
Returns available tools |
tools/call |
Executes a tool |
| Tool | Description |
|---|---|
clawgate_read_file |
Read file contents |
clawgate_write_file |
Write file contents |
clawgate_list_directory |
List directory entries |
clawgate_stat |
Get file/directory metadata |
clawgate_git |
Run git commands on the primary machine |
clawgate_tool |
Invoke a registered tool on the primary machine |
clawgate_tool_list |
List available tools on the primary machine |
// Request
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}
// Response
{"jsonrpc": "2.0", "id": 1, "result": {"capabilities": {...}}}
// Request
{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
// Response
{"jsonrpc": "2.0", "id": 2, "result": {"tools": [...]}}
// Request
{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {
"name": "clawgate_read_file",
"arguments": {"path": "/home/mario/readme.txt"}
}}
// Response
{"jsonrpc": "2.0", "id": 3, "result": {"content": "..."}}
// Request (git)
{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {
"name": "clawgate_git",
"arguments": {"path": "/home/mario/projects/myapp",
"args": ["status", "--short"]}
}}
// Response
{"jsonrpc": "2.0", "id": 4, "result": {"content": "M src/main.zig\n"}}A common setup where a Mac Mini runs AI workloads and accesses files on your MacBook over the local network.
Network Topology:
+------------------+ +------------------+
| MacBook (LAN) | | Mac Mini (LAN) |
| 192.168.1.10 | | 192.168.1.100 |
|------------------| |------------------|
| Resource Daemon |-------->| Agent Daemon |
| (connects out) | :53280 | (listens) |
| | | MCP Server |
+------------------+ +--------+---------+
|
v
+------------------+
| Claude Code |
+------------------+
Setup Steps:
-
Generate keys (MacBook):
clawgate keygen
-
Grant access (MacBook):
clawgate grant --read --ttl 24h ~/projects > token.txt
-
Prepare agent machine (Mac Mini):
mkdir -p ~/.clawgate/keys -
Copy files to Mac Mini (from MacBook):
scp token.txt mini:~/ scp ~/.clawgate/keys/public.key mini:~/.clawgate/keys/
The agent needs your public key to verify token signatures.
-
Store token (Mac Mini):
clawgate token add "$(cat ~/token.txt)" -
Start agent daemon (Mac Mini):
clawgate --mode agent --listen 0.0.0.0:53280
-
Start resource daemon (MacBook):
clawgate --mode resource --connect 192.168.1.100:53280
-
Test access (Mac Mini):
clawgate cat ~/projects/myapp/README.md
Security Notes:
- Both machines should be on a trusted network segment
- Consider firewall rules to restrict port 53280 access
- Use short TTL tokens (1-24 hours) for regular work
Running Claude Code on a VPS while accessing local files through an SSH tunnel.
Network Topology:
+------------------+ +------------------+
| Local Laptop | | VPS |
| (behind NAT) | | (public IP) |
|------------------| SSH Tunnel |------------------|
| Resource Daemon |=====================>| Agent Daemon |
| connects to | localhost:53280 | listens :53280 |
| localhost:53280 | | MCP Server |
+------------------+ +--------+---------+
|
v
+------------------+
| Claude Code |
+------------------+
Setup Steps:
-
Establish SSH tunnel (local laptop):
ssh -R 53280:localhost:53280 user@vps.example.com
This forwards VPS port 53280 to your local machine.
-
Generate keys and grant access (local laptop):
clawgate keygen clawgate grant --read --write --ttl 8h ~/code > token.txt
-
Prepare agent machine (VPS):
mkdir -p ~/.clawgate/keys -
Copy files to VPS (from local laptop):
scp token.txt user@vps.example.com:~/ scp ~/.clawgate/keys/public.key user@vps.example.com:~/.clawgate/keys/
The agent needs your public key to verify token signatures.
-
Store token (VPS):
clawgate token add "$(cat ~/token.txt)" -
Start agent daemon (VPS):
clawgate --mode agent --listen 127.0.0.1:53280
Note: Listen only on localhost for security.
-
Start resource daemon (local laptop):
clawgate --mode resource --connect localhost:53280
Connection goes through the SSH tunnel.
Security Considerations:
- The SSH tunnel provides an additional encryption layer
- Agent daemon should only listen on 127.0.0.1 (not 0.0.0.0)
- Use SSH key authentication, not passwords
- Consider shorter TTL tokens when VPS security is uncertain
- VPS firewall should block external access to port 53280
Running the agent daemon in a Docker container.
Dockerfile:
FROM debian:bookworm-slim
COPY clawgate /usr/local/bin/
COPY public.key /etc/clawgate/
RUN mkdir -p /var/lib/clawgate/tokens
EXPOSE 53280
CMD ["clawgate", "--mode", "agent", \
"--listen", "0.0.0.0:53280", \
"--token-dir", "/var/lib/clawgate/tokens"]Docker Compose:
version: '3.8'
services:
clawgate-agent:
build: .
ports:
- "53280:53280"
volumes:
- ./tokens:/var/lib/clawgate/tokens
restart: unless-stoppedNotes:
- Mount token directory as volume for persistence
- Copy resource's public key into container at build time (agent needs it to verify token signatures)
- Network mode may need adjustment for your setup
# Run agent daemon
clawgate --mode agent [options]
--listen <addr:port> Listen address (default: 0.0.0.0:53280)
--token-dir <path> Token directory (default: ~/.clawgate/tokens)
# Run resource daemon
clawgate --mode resource [options]
--connect <host:port> Connect to agent (required)
--public-key <path> Public key (default: ~/.clawgate/keys/public.key)
--resource-id <id> Resource identifier (default: clawgate-resource)
# Run MCP server
clawgate mcp-server [options]
--token-dir <path> Token directory (default: ~/.clawgate/tokens)# Generate Ed25519 keypair
clawgate keygen [options]
-o, --output <dir> Output directory (default: ~/.clawgate/keys)
-f, --force Overwrite existing keys
# Grant capability token
clawgate grant [options] [path]
-r, --read Allow read (includes list, stat)
-w, --write Allow write
--list Allow list only
--stat Allow stat only
--git Git read-only (+ read, list, stat)
--git-write Git read+write (+ file write)
--git-full Git full access (+ push/pull/fetch)
--tool <name> Grant access to a registered tool
--tools-all Grant access to all registered tools
-t, --ttl <duration> Token lifetime (default: 24h)
-k, --key <path> Secret key path
--issuer <id> Issuer identity
--subject <id> Subject identityTTL formats: 1h, 24h, 7d, 3600s, 1800m, or plain seconds.
# Add token to store
clawgate token add [token]
-d, --token-dir <dir> Token directory
# Token can be argument or stdin
# List stored tokens
clawgate token list
-d, --token-dir <dir> Token directory
--json Output as JSON
# Remove token
clawgate token remove <id>
-d, --token-dir <dir> Token directory
# Show token details
clawgate token show <id>
-d, --token-dir <dir> Token directoryExample output:
$ clawgate token list
Stored tokens (6):
ID: cg_9ae7ce62f4a5b869a8c120fa
Issuer: clawgate:resource
Subject: clawgate:agent
Scope: ~/space/ai/remembra/** [read, list, stat, git]
Expires: 2026-02-08T05:51:26Z
Status: Valid
...
$ clawgate token show cg_7ab54be138936dfb8d29b81d
Token: cg_7ab54be138936dfb8d29b81d
Issuer: clawgate:resource
Subject: clawgate:agent
Issued: 2026-02-07T06:02:28Z
Expires: 2026-02-08T06:02:28Z
Capabilities:
- files: ~/space/ai/tiger-style [read, list, stat, git]
Status: Valid
# Read file
clawgate cat <path>
-d, --token-dir <dir> Token directory
--offset <n> Starting byte offset
--length <n> Maximum bytes to read
# List directory
clawgate ls <path>
-d, --token-dir <dir> Token directory
--depth <n> Listing depth (default: 1)
-l Long format with sizes
# Write file
clawgate write <path>
-d, --token-dir <dir> Token directory
-c, --content <text> Content (or stdin)
-a, --append Append mode
--create Fail if file exists
# Get file/directory info
clawgate stat <path>
-d, --token-dir <dir> Token directory
--json Output as JSON# Run git commands
clawgate git <repo-path> <git-args...>
-d, --token-dir <dir> Token directory
# Examples:
clawgate git ~/projects/myapp status
clawgate git ~/projects/myapp diff HEAD~3
clawgate git ~/projects/myapp log --oneline -20
clawgate git ~/projects/myapp commit -m "fix bug"
clawgate git ~/projects/myapp push origin main# Register a tool (resource machine)
clawgate tool register <name> --command "..." [options]
--command <cmd> Command to execute (required)
--scope <paths> Semicolon-separated scope paths (relative to $HOME)
--allow-args <arg> Allowed flag (repeatable, sets allowlist mode)
--deny-args <arg> Denied flag (repeatable)
--timeout <secs> Timeout in seconds (default: 30)
--max-output <bytes> Max output bytes (default: 65536)
--description <text> Tool description
--example <text> Usage example (repeatable)
# Management
clawgate tool ls List registered tools
clawgate tool info <name> Show tool details
clawgate tool update <name> Update tool config (same options as register)
clawgate tool remove <name> Remove a tool
clawgate tool test <name> [args] Test tool locally (no daemons needed)
clawgate tool generate Generate skill files
# Remote discovery and invocation (agent machine)
clawgate tool remote-list List tools available via daemon
clawgate tool <name> [args] Invoke tool via daemon# View audit log info
clawgate auditAudit events are logged to ~/.clawgate/logs/audit.log:
2026-02-07T14:30:45Z AUDIT req=req_12345 op=read path=/home/mario/file.txt success=true
2026-02-07T14:30:46Z AUDIT req=req_12346 op=write path=/etc/shadow success=false error=SCOPE_VIOLATION
Events are also printed to stderr by the resource daemon.
| Limit | Value | Configurable |
|---|---|---|
| Maximum file size | 100 MB | No |
| Truncation threshold | 512 KB | No |
| Maximum token payload | 16 KB | No |
| Parameter | Value | Configurable |
|---|---|---|
| Default port | 53280 | Yes |
| Max message size | 100 MB | No |
| Length prefix | 4 bytes | No |
| Parameter | Value |
|---|---|
| Session ID length | 37 chars (sess_ + 32 hex) |
| Token ID length | 27 chars (cg_ + 24 hex) |
| Nonce counter max | 2^64 messages per session |
- Minimal scope - Grant only the paths needed, not entire home directory
- Minimal TTL - Use shortest practical lifetime (hours, not days)
- Minimal operations - Don't grant write if only read is needed
- Secure transfer - Use encrypted channel to transfer tokens
- Outbound-only - Resource daemon initiates, no inbound to trusted machine
- E2E encryption - Safe over untrusted networks
- Forward secrecy - Fresh keys per session
- Key permissions - Secret key should be 0600 (owner read-only)
- Memory zeroing - All secrets zeroed after use
- No logging - Secrets never appear in logs
ClawGate supports active token revocation via a resource-side revocation
list at ~/.clawgate/revoked.json. The list is reloaded on every incoming
request, so revocations take effect immediately without daemon restart.
clawgate revoke <token-id> --reason "compromised"
clawgate revoke --all --reason "key rotation"
clawgate revoked ls
clawgate revoked clean # Remove expired entriesWhen a revoked token is used, the resource daemon rejects the request
with TOKEN_REVOKED. The agent daemon automatically removes revoked
tokens from its local store on first rejection.
| Code | HTTP Equiv. | Description |
|---|---|---|
| INVALID_TOKEN | 401 | Token malformed or signature invalid |
| TOKEN_EXPIRED | 401 | Token past expiration time |
| TOKEN_REVOKED | 401 | Token revoked via revocation list |
| SCOPE_VIOLATION | 403 | Path not in token scope |
| INVALID_OP | 400 | Unknown operation |
| INVALID_PATH | 400 | Path failed canonicalization |
| INVALID_REQUEST | 400 | Malformed request JSON |
| FILE_NOT_FOUND | 404 | Target path doesn't exist |
| ACCESS_DENIED | 403 | OS permission denied or forbidden path |
| FILE_TOO_LARGE | 413 | File exceeds 100 MB |
| NOT_A_FILE | 400 | Operation requires file, got directory |
| NOT_A_DIRECTORY | 400 | Operation requires directory, got file |
| IS_SYMLINK | 403 | Symlinks not permitted |
| GIT_ERROR | 500 | Git command execution failed |
| GIT_BLOCKED | 403 | Git command or flag blocked by allowlist |
| GIT_NOT_REPO | 400 | Target path is not a git repository |
| GIT_TIMEOUT | 504 | Git command timed out |
| TOOL_DENIED | 403 | No tool capability or tool not registered |
| TOOL_ERROR | 500 | Tool subprocess execution failed |
| TOOL_TIMEOUT | 504 | Tool exceeded configured timeout |
| ARG_BLOCKED | 403 | Tool argument blocked by allow/deny list |
| PATH_BLOCKED | 403 | Path argument outside tool scope |
| INTERNAL_ERROR | 500 | Unexpected server error |