Skip to content

Latest commit

 

History

History
1641 lines (1341 loc) · 47.6 KB

File metadata and controls

1641 lines (1341 loc) · 47.6 KB

ClawGate Design Document

Version: 0.3.3 Status: Implementation Complete

Executive Summary

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

Document Scope

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.

Architecture Overview

System Components

ClawGate consists of four main components:

  1. 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.

  2. 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.

  3. MCP Server - Model Context Protocol server running on the isolated machine. Provides JSON-RPC 2.0 interface over stdio for AI tool integration.

  4. CLI Tools - Commands for token management, file operations, key generation, and daemon control.

Component Diagrams

OpenClaw via Skill

+-------------------------+                    +-------------------------+
|    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)       |  |
                                               |   +------------------+  |
                                               +-------------------------+

Data Flow

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.

Claude via MCP Server

+-------------------------+                    +-------------------------+
|    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.)   |  |
                                               |   +------------------+  |
                                               +-------------------------+

Data Flow

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.

Connection Model

The connection model is designed for security:

  1. Resource daemon initiates - The trusted machine always connects outbound. No inbound connections to your laptop are required.

  2. Agent daemon listens - The isolated machine accepts connections on port 53280 (configurable).

  3. Single active connection - One resource daemon connects to one agent daemon at a time.

  4. Persistent connection - The connection remains open for the session duration, with automatic reconnection on disconnect.

Security Model

Threat Model

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)

Defense Layers

Layer 1: Network Encryption (E2E)

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.

Layer 2: Capability Tokens (JWT)

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

Layer 3: Path Security

Multiple layers protect against path-based attacks:

  1. Canonicalization - Paths are normalized to remove .., ., and // before any checks. Attempts to escape via traversal are rejected.

  2. Symlink Rejection - All file operations reject symbolic links, preventing symlink-based scope escapes.

  3. Forbidden Paths - Hardcoded patterns block access to sensitive locations regardless of token scope.

  4. 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.

  5. 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.

Layer 4: Audit Logging

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>]

Cryptographic Primitives

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 Management

Key Generation:

clawgate keygen -o ~/.clawgate/keys

Creates:

  • 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.

Forbidden Paths

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.

Capability Token Format

JWT Structure

Tokens use the standard JWT format:

BASE64URL(header).BASE64URL(payload).BASE64URL(signature)

Header

{
  "alg": "EdDSA",
  "typ": "JWT"
}

Only EdDSA (Ed25519) algorithm is accepted. Any other algorithm is rejected.

Payload Claims

{
  "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)

Scope Patterns

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

Token Lifecycle

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

Token Validation Flow

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

Protocol Specification

Transport Layer

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

Handshake Protocol

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:

  1. Compute X25519 shared secret: shared = X25519(my_secret, their_public)
  2. Derive session key: key = HKDF-SHA256(salt="clawgate-e2e-v1", ikm=shared, info=session_id)
  3. Initialize nonce counter to 0

All subsequent messages are encrypted.

Encrypted Message Format

+-------------+-------------------+-----------+
| 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)

Request Format

{
  "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

Response Format

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"]
    }
  ]
}

Error Codes

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

Git Operations

Overview

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.

Permission Tiers

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.

Grant CLI Flags

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_remote

Scope 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.

Request Format

{
  "id": "req_abc123",
  "token": "<jwt>",
  "op": "git",
  "params": {
    "path": "/home/mario/projects/myapp",
    "args": ["diff", "--stat", "HEAD~3"]
  }
}

Response Format

{
  "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.

Command Allowlists

Tier 1: git (read-only)

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)

Tier 2: git_write (mutating)

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)

Tier 3: git_remote (remote)

All of tier 1 + tier 2 plus:

push, pull, fetch, remote (add/remove/set-url), submodule, clone

Blocked Flags

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)

Validation Flow

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)

Security Considerations

  1. Git runs on the resource daemon (trusted machine) using the user's own git config and credentials
  2. Command allowlists prevent arbitrary code execution via git hooks and exec flags
  3. Forbidden paths still apply - git can't access ~/.ssh/ etc.
  4. Scope validation - git only runs in token-scoped directories
  5. Output truncation at 512 KB prevents memory exhaustion
  6. --git-dir/--work-tree blocked prevents pointing git at repos outside scope

Custom Tool Operations

Overview

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.

Tool Registration

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

Scope Model

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")

Security Architecture

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)

Layer 1: Flag Validation

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.

Layer 2: Path Scanning

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:

  1. Expand tilde to $HOME (consistent with path.expand())
  2. Resolve relative paths against $HOME (the CWD base)
  3. Canonicalize via scope.canonicalizePath() - rejects .. escapes, null bytes, non-absolute results
  4. Check forbidden paths via isForbiddenPath() - .ssh, .gnupg, .aws, etc. are blocked even if within scope
  5. Check scope entries - the canonical path must fall within at least one $HOME/<scope_entry> via scope.isWithin()

If no scope entry matches, the path is rejected with PATH_BLOCKED.

Layer 3: CWD Confinement

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.

Request Format

{
  "id": "req_abc123",
  "token": "<jwt>",
  "op": "tool",
  "params": {
    "tool_name": "rg",
    "tool_args": ["pattern", "~/projects/webapp/src"],
    "input": null
  }
}

Response Format

{
  "id": "req_abc123",
  "ok": true,
  "result": {
    "tool_name": "rg",
    "stdout": "src/main.zig:42: pattern match\n",
    "stderr": "",
    "exit_code": 0,
    "truncated": false
  }
}

Tool Discovery

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": {}
}

Execution Model

  • Commands are split into argv tokens and executed via std.process.run or std.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 truncated flag indicates when output was cut short
  • Exit code is captured from the child process (signals mapped to 128)

Known Limitations

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.

MCP Integration

Overview

The MCP (Model Context Protocol) server enables AI tools to access ClawGate capabilities via JSON-RPC 2.0 over stdio.

Methods

Method Description
initialize Returns server capabilities
tools/list Returns available tools
tools/call Executes a tool

Tools

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

Example Session

// 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"}}

Deployment Scenarios

Scenario 1: Private LAN (Mac Mini + MacBook)

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:

  1. Generate keys (MacBook):

    clawgate keygen
  2. Grant access (MacBook):

    clawgate grant --read --ttl 24h ~/projects > token.txt
  3. Prepare agent machine (Mac Mini):

    mkdir -p ~/.clawgate/keys
  4. 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.

  5. Store token (Mac Mini):

    clawgate token add "$(cat ~/token.txt)"
  6. Start agent daemon (Mac Mini):

    clawgate --mode agent --listen 0.0.0.0:53280
  7. Start resource daemon (MacBook):

    clawgate --mode resource --connect 192.168.1.100:53280
  8. 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

Scenario 2: VPS with SSH Tunnel

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:

  1. Establish SSH tunnel (local laptop):

    ssh -R 53280:localhost:53280 user@vps.example.com

    This forwards VPS port 53280 to your local machine.

  2. Generate keys and grant access (local laptop):

    clawgate keygen
    clawgate grant --read --write --ttl 8h ~/code > token.txt
  3. Prepare agent machine (VPS):

    mkdir -p ~/.clawgate/keys
  4. 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.

  5. Store token (VPS):

    clawgate token add "$(cat ~/token.txt)"
  6. Start agent daemon (VPS):

    clawgate --mode agent --listen 127.0.0.1:53280

    Note: Listen only on localhost for security.

  7. 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

Scenario 3: Docker Container

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-stopped

Notes:

  • 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

CLI Reference

Daemon Commands

# 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)

Capability Commands

# 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 identity

TTL formats: 1h, 24h, 7d, 3600s, 1800m, or plain seconds.

Token Management

# 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 directory

Example 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

File Operations

# 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

Git Operations

# 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

Tool Registry

# 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

Monitoring

# View audit log info
clawgate audit

Audit 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.

Operational Limits

File Operations

Limit Value Configurable
Maximum file size 100 MB No
Truncation threshold 512 KB No
Maximum token payload 16 KB No

Network

Parameter Value Configurable
Default port 53280 Yes
Max message size 100 MB No
Length prefix 4 bytes No

Cryptographic

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

Security Considerations

Token Handling Best Practices

  1. Minimal scope - Grant only the paths needed, not entire home directory
  2. Minimal TTL - Use shortest practical lifetime (hours, not days)
  3. Minimal operations - Don't grant write if only read is needed
  4. Secure transfer - Use encrypted channel to transfer tokens

Network Security

  1. Outbound-only - Resource daemon initiates, no inbound to trusted machine
  2. E2E encryption - Safe over untrusted networks
  3. Forward secrecy - Fresh keys per session

Secret Management

  1. Key permissions - Secret key should be 0600 (owner read-only)
  2. Memory zeroing - All secrets zeroed after use
  3. No logging - Secrets never appear in logs

Revocation

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 entries

When 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.

Appendix A: Error Code Reference

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