This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
AgentFense is a filesystem-backed sandbox system that enforces fine-grained, path-based access control for AI agents. It lets you run untrusted code against a real codebase while enforcing least-privilege access at the file level using four permission levels: none (invisible), view (list-only), read, and write.
# Generate protobuf code (required after .proto changes)
./scripts/gen-proto.sh
# Build the server
go build -o bin/agentfense-server ./cmd/agentfense-server
# Run the server (gRPC :9000, REST :8080)
./bin/agentfense-server -config configs/agentfense-server.yaml
# Run with specific runtime (overrides config)
./bin/agentfense-server -runtime bwrap
./bin/agentfense-server -runtime docker
./bin/agentfense-server -runtime mock # For testing without isolation# Run all tests
go test ./...
# Run specific package tests
go test ./internal/fs/...
go test ./internal/runtime/bwrap/...
# Run with verbose output
go test -v ./internal/fs/
# Run with coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Integration tests
go test ./test/integration/...# Install Python SDK in development mode
cd sdk/python
pip install -e .
# Run Python SDK tests (if they exist)
cd sdk/python
pytest- Client Layer: Go SDK (TODO), Python SDK, REST API
- Service Layer: gRPC server + REST gateway (grpc-gateway)
- Runtime Layer: Sandbox Manager, Permission Engine, Executor
- Isolation Layer: bwrap/Docker runtimes, FUSE filesystem, Delta Layer (COW)
Permission System (internal/fs/permission.go):
- Implements pattern-based access control with deterministic priority
- Rules sorted by: explicit Priority → PatternType (file > directory > glob) → pattern specificity
- More specific patterns always win (e.g.,
/secrets/public.keybeats/secrets/**)
FUSE Filesystem (internal/fs/fuse.go):
- Mounts codebases as FUSE filesystems with permission enforcement
- Every file operation checked against permission rules
- Paths with
nonepermission are completely invisible (don't appear inls)
Delta Layer (COW) (internal/fs/delta.go):
- Provides Copy-On-Write isolation for multi-sandbox write safety
- Writes go to per-sandbox delta directories
- Reads check delta first, fallback to source
- Deletes create whiteout markers (
.wh.*files) - Syncs delta → source on exec() completion using Last-Writer-Wins
Runtime Abstraction (internal/runtime/runtime.go):
- Three implementations:
bwrap(lightweight),docker(full isolation),mock(testing) - Key interfaces:
Runtime(lifecycle),Executor(commands),SessionManager(stateful shells) - Sessions maintain persistent shell processes with working directory and environment state
Server Layer (internal/server/server.go):
- Combines gRPC and REST endpoints (grpc-gateway handles translation)
- Manages codebase storage and sandbox lifecycle
- Coordinates between runtime, filesystem, and permission engine
Sandbox Creation:
- Client creates codebase → uploads files to storage
- Client creates sandbox with permission rules
- Server validates rules, creates delta directory
- Runtime creates FUSE mount with permission engine
- Runtime starts isolation (bwrap namespace or Docker container)
Command Execution:
- Client sends exec request
- Server validates sandbox is running
- FUSE filesystem enforces permissions on all file access
- Runtime executes command in isolated environment
- Delta layer captures writes to per-sandbox directory
- On completion, changes sync to source (LWW if conflicts)
Permission rules use pattern matching with three types:
- file: Exact match (
/config.yaml) - highest priority - directory: Prefix match (
/docs/matches/docs/file.txt) - glob: Wildcard match (
**/*.py,/secrets/**)
Rules are automatically prioritized. You don't need to set Priority manually unless overriding.
rules := []types.PermissionRule{
{Pattern: "**/*", Type: types.PatternGlob, Permission: types.PermRead}, // Default: read all
{Pattern: "/secrets/**", Type: types.PatternGlob, Permission: types.PermNone}, // Hide secrets
{Pattern: "/docs/**", Type: types.PatternGlob, Permission: types.PermWrite}, // Writable docs
}- Implement
runtime.RuntimeWithExecutorinterface - Handle sandbox lifecycle: Create → Start → Exec → Stop → Destroy
- Mount FUSE filesystem at the mount point provided in
SandboxConfig - Execute commands within the isolated environment
- Optional: Implement
SessionManagerfor stateful shell support
See internal/runtime/bwrap/bwrap.go or internal/runtime/docker/docker.go as reference.
FUSE tests require actual mount points. The test setup:
- Creates temporary directories for source and mount
- Starts FUSE server in goroutine
- Waits for mount to be ready
- Runs test operations
- Unmounts and cleans up
Use MountWithReady() to get notification when mount succeeds.
Server config: configs/agentfense-server.yaml
Key settings:
runtime.type:bwrap,docker, ormockstorage.codebase_path: Where codebases are stored (must be absolute for Docker)storage.mount_path: Base for FUSE mounts and delta directoriesruntime.docker.enable_networking: Control network access in sandboxes
Override via flags:
./bin/agentfense-server -runtime docker -grpc-addr :9001All storage paths MUST be absolute when using Docker runtime (required for bind mounts). The server automatically normalizes paths on startup via normalizeStoragePaths() in cmd/agentfense-server/main.go.
The permission system uses a sophisticated priority algorithm:
- Explicit
Priorityfield (if set) - Pattern type: file (3) > directory (2) > glob (1)
- Pattern specificity: exact path > prefix match > glob
You rarely need to set Priority manually. The system auto-prioritizes based on specificity.
Delta sync happens on exec() completion, not continuously. If sandbox crashes, delta changes may be lost. This is intentional - sandboxes are ephemeral.
The mock runtime (-runtime mock) executes no commands and returns empty results. Use it for testing server/API logic without actual isolation. If you see exit_code=0 but empty stdout for all commands, you're probably using mock runtime.
FUSE mount errors: Ensure fuse or osxfuse is installed. On macOS, may need to allow kernel extension.
Permission denied in sandbox: Check permission rules match actual paths. Paths must start with /workspace inside sandbox.
Docker containers not cleaning up: Docker runtime should auto-remove containers. Check docker ps -a and manually remove if needed.
Empty output from commands: If using mock runtime, switch to bwrap or docker for actual execution.
This project follows TDD (Test-Driven Development):
- RED: Write a failing test for the feature
- GREEN: Write minimal code to make test pass
- REFACTOR: Improve code quality while keeping tests green
When modifying .proto files, always regenerate with ./scripts/gen-proto.sh before building.