All file-bearing tools (repo.read_file, repo.apply_patch, repo.search, git.diff) route paths through src/core/policy/sandbox.ts:
- Path canonicalization:
resolve()resolves relative paths against repo root - Traversal rejection: Paths resolving outside repo root are rejected (e.g.,
../../etc/passwd) - Symlink escape detection:
realpathSyncverifies resolved symlink targets stay within sandbox - Null byte rejection: Paths containing
\0are rejected before any filesystem operation - Absolute path containment: Absolute paths outside repo root are rejected
- Max path length: 4096-byte limit on input paths
Implementation: Sandbox.resolve() — called before every filesystem operation.
repo.runusesspawn()withshell: false— args are passed as arrays, never interpolatedrepo.searchusesexecFile()(no shell by default) — ripgrep invoked directlygit.statusandgit.diffuseexecFile()(no shell by default) — git invoked directly- No shell-based execution anywhere in the codebase
- Fixed enum of allowed commands:
test,lint,typecheck,build,smoke - Per-command allowlisted argument sets (e.g.,
testallows--reporter=dot) - Disallowed arguments rejected before spawn
- Minimal environment: only
PATH,HOME,USER,LANG,TMPDIR,NODE_ENVpassed
Each tool enforces output caps:
| Tool | Cap | Mechanism |
|---|---|---|
repo.search |
200 results max | max_results parameter, capped at 200 |
repo.read_file |
10MB max | max_bytes parameter, capped at 10MB |
repo.apply_patch |
N/A | Input-bounded (patch size) |
repo.run |
200KB stdout + 200KB stderr | Buffer-based collection with byte caps |
git.status |
500 entries max | max_entries parameter, capped at 500 |
git.diff |
2MB max | max_bytes parameter, capped at 2MB |
All caps enforce hard maximums — user-requested values are Math.min(requested, cap).
- Default timeout: 120 seconds
- Hard cap: 300 seconds (5 minutes)
- SIGTERM sent at timeout, SIGKILL after 2s grace period
- Timed-out commands return
E_TIMEOUTerror code
- Pure JS: Uses
difflibrary — no shellpatchcommand - Validation pass first: All paths sandbox-checked, all files read, all patches computed before any writes
- Atomic writes: If any write fails, undo stack rolls back all completed writes
- No partial application: Either all files patched or none
- Dry run mode: Validate without writing via
dry_run: true
- All tools return
ToolEnvelope<T>withok,data/error, andprovenance - Array outputs sorted deterministically (lexicographic by path)
- Timestamps in ISO 8601 format
- Duration tracked in milliseconds
- One JSONL line per tool call:
{ts, tool, ok, duration_ms, error_code?, repo_root} - Controlled via
GHOSTLINK_LOGenv var:stdout(default),file,off - File mode: writes to
logs/ghostlink.jsonl, auto-rotates at 10MB
- OAuth 2.1
- Tenant separation
- Rate limiting
- Structured audit logging (remote transport)
- Memory write policies
- Capability-based authorization