This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Read AGENTS.md before any code change. It is the authoritative engineering protocol covering architecture, naming conventions, anti-patterns, change playbooks, and validation requirements.
# Requires exactly Zig 0.16.0 (verify: zig version)
zig build # dev build
zig build -Doptimize=ReleaseSmall # release build (target: <1 MB binary)
zig build test --summary all # run all 5,300+ tests (must pass with 0 leaks)
zig fmt src/ # format all source files
zig fmt --check src/ # check formatting (used by pre-commit hook)Primary validation command is zig build test --summary all (project-wide). Individual files can still be run with zig test <file>.zig when needed.
zig build -Dchannels=telegram,cli # compile only specific channels (default: all)
zig build -Dengines=base,sqlite # compile only specific memory engines (default: base,sqlite)
zig build -Dtarget=x86_64-linux-musl # cross-compile for target triple
zig build -Dversion=2026.3.1 # override CalVer version stringChannel tokens: all, none, or comma-separated names (cli, telegram, discord, slack, signal, matrix, web, nostr, irc, email, imessage, whatsapp, mattermost, lark, dingtalk, line, onebot, qq, maixcam).
Engine tokens: base/minimal (enables none, markdown, memory, api), sqlite, lucid, redis, lancedb, postgres, all.
Activate once per clone:
git config core.hooksPath .githooks- pre-commit: blocks if
zig fmt --check src/fails - pre-push: blocks if
zig build test --summary allfails
NullClaw is an autonomous AI assistant runtime written in Zig 0.16.0. Hard constraints: 678 KB binary, ~1 MB peak RSS, <2 ms startup. Every dependency and abstraction has a measurable size/memory cost. Only two external dependencies: vendored SQLite (with build-time SHA256 hash verification) and websocket.zig (pinned commit).
The entire codebase is vtable-driven. All major subsystems use ptr: *anyopaque + vtable: *const VTable for pluggable implementations. Extending NullClaw means implementing a vtable struct and registering it in the subsystem's factory (see AGENTS.md section 7 for playbooks).
Critical ownership rule: callers must OWN the implementing struct (local var or heap-alloc). Never return a vtable interface pointing to a temporary -- the pointer will dangle.
Defined in src/root.zig. Phases mirror deployment dependencies:
- Core:
bus,config,util,platform,version,state,json_util,http_util - Agent:
agent,session,providers,memory - Networking:
gateway,channels - Extensions:
security,cron,health,tools,identity,cost,observability,heartbeat,runtime,mcp,subagent,auth,multimodal,agent_routing - Hardware/Integrations:
hardware,peripherals,rag,skillforge,tunnel,voice
src/main.zig- CLI command routing (agent,gateway,onboard,doctor,status,service,cron,channel,memory,skills,hardware,migrate,workspace,capabilities,models,auth,update,history)src/root.zig- Module hierarchy and public API exports (also serves as library root)src/config.zig- JSON config loading (~30 sub-config structs fromconfig_types.zig, loads from~/.nullclaw/config.json)src/agent.zig- Agent orchestration (delegates tosrc/agent/root.zig)src/gateway.zig- HTTP gateway server (rate limiting, pairing, webhooks)src/daemon.zig- Supervisor with exponential backoff for gateway mode
src/providers/- AI model providers. 9 core implementations + 41+ OpenAI-compatible services viacompatible.zig. Factory infactory.zig, single source of truth for provider URLs and auth styles.src/channels/- Messaging channels. Each implementsChannel.VTable(start,stop,send,name,healthCheck). Factory inroot.zig.src/tools/- Tool implementations. Each implementsTool.VTable(execute,name,description,parameters_json). Tools receive args asJsonObjectMapand returnToolResult. Factory inroot.zig.src/memory/- Layered architecture: engines (SQLite, Markdown, LRU, Redis, PostgreSQL, LanceDB, Lucid, ClickHouse, API, None) and retrieval (hybrid search, RRF, embeddings). Engines conditionally compiled via build flags.src/security/- Policy enforcement (policy.zig), pairing (pairing.zig), encrypted secrets (secrets.zig), sandbox backends (landlock.zig,firejail.zig,bubblewrap.zig,docker.zig,detect.zig).src/agent/- Agent loop internals:dispatcher.zig(tool call parsing),compaction.zig(history trimming),prompt.zig(system prompt builder),memory_loader.zig(context injection),commands.zig(agent-mode commands). Config defaults aremax_tool_iterations = 1000andmax_history_messages = 100(seesrc/config_types.zig).
- Keep canonical tool names in the runtime and prompt layer. Provider-specific quirks should be normalized at the provider boundary when possible.
src/providers/ollama.zigalready normalizes common local-model tool-name drift such astool.shell->shell,tools.file_read->file_read, andscheduler_tool/schedule_tool->schedule.- If a local model invents another wrapper-style tool name, prefer extending the Ollama normalization helper and adding a regression test instead of teaching alternate names to the tool registry or prompt text.
Concrete implementations depend inward on vtable interfaces, config, and util. Never import across subsystems (e.g., provider code must not import channel internals).
Config loads from ~/.nullclaw/config.json. Runtime behavior is then adjusted by NULLCLAW_* environment overrides (see Config.applyEnvOverrides() in src/config.zig). Types are defined in src/config_types.zig and re-exported from src/config.zig.
Config.load() heap-allocates an internal ArenaAllocator. Always call defer cfg.deinit() to free. In tests, wrap in a parent arena:
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var cfg = try Config.load(arena.allocator());
defer cfg.deinit();Key config sections: models.providers (API keys/endpoints), agents (named agent configs), channels (per-channel settings), memory (backend/search/lifecycle), gateway (port/host/pairing), security (sandbox/audit/autonomy), autonomy (level/limits/allowlists), runtime (native/docker/wasm).
std.io.getStdOut()does NOT exist. Usestd.fs.File.stdout().- HTTP client:
std.http.Client.fetch()withstd.Io.Writer.Allocating. - Child processes:
std.process.Child.init(argv, allocator),.Pipe(capitalized). ArrayListUnmanaged: init with.empty, pass allocator to every method.ChaCha20Poly1305.decrypt: use stack buffer thenallocator.dupe()(heap buffer segfaults on macOS).SQLITE_TRANSIENTin auto-translated C code: useSQLITE_STATIC(null) instead.- When unsure about API, search
src/for existing usage rather than guessing.
Run zig env to locate Zig source directories. .std_dir points to the standard library, .lib_dir to the broader lib tree. Read the source directly to verify struct fields, function signatures, and available methods.
- All tests use
std.testing.allocator(leak-detecting GPA). Every allocation must be freed withdefer. - Use
builtin.is_testguards to skip side effects (spawning processes, opening browsers, real hardware I/O). Return mock data instead (e.g.,return "test-refreshed-token"). - Tests must be deterministic and reproducible across macOS and Linux.
- Vendored SQLite hashes are validated at build time.
- Use
std.testing.tmpDir(.{})withdefer tmp.cleanup()for file-based test fixtures. - Contract tests in
src/memory/engines/contract_test.zigverify all memory backends satisfy the same vtable invariants. Follow this pattern when adding new backends. - Test helpers (e.g.,
TestHelperstructs withdummyConfig()/initTestChannel()) are defined within each module. Prefer this pattern over shared test utilities. - Test naming:
subject_expected_behavior(e.g.,"sendUrl constructs correct URL").
CalVer format: YYYY.M.D (e.g., v2026.2.26). Defined in build.zig.zon.
Tests run on Ubuntu (x86_64), macOS (aarch64), and Windows (x86_64). Release builds target 7 platforms including linux-riscv64. Docker images published to ghcr.io (linux/amd64, linux/arm64).
Multi-stage build: Alpine builder with Zig, then minimal Alpine runtime. Runs as non-root (uid 65534) by default. Use --target release-root for root access.
docker-compose --profile gateway up # HTTP gateway daemon
docker-compose --profile agent up # interactive agentflake.nix provides a dev shell with Zig and ZLS. Activate with direnv allow (uses .envrc).
MIT License.