> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗

Back to Articles

NullClaw: Building a 678 KB AI Assistant That Boots in 2 Milliseconds

[ View on GitHub ]

NullClaw: Building a 678 KB AI Assistant That Boots in 2 Milliseconds

Hook

While OpenAI's Assistant API deployment footprint starts at 500MB and takes seconds to initialize, NullClaw delivers a complete AI assistant infrastructure in 678 KB that boots in under 2 milliseconds. This isn't vaporware—it's production Zig code running on $5 hardware.

Context

The AI assistant landscape has a resource problem. Langchain requires 100MB+ of memory just to start. AutoGPT assumes you're running on a developer workstation with gigabytes to spare. Even lightweight frameworks like Rasa need 250MB minimum. This works fine for cloud deployments and desktop applications, but it completely excludes an enormous class of devices: edge hardware, IoT sensors, embedded systems, and the billions of microcontrollers that could benefit from local AI capabilities.

The traditional answer has been to offload everything to the cloud—let the Raspberry Pi make API calls to OpenAI or Anthropic and keep local footprint minimal. But this creates latency, privacy concerns, connectivity dependencies, and ongoing API costs. What if you could run a fully-featured AI assistant infrastructure locally, on hardware that costs less than a sandwich, with no network dependency? NullClaw emerged from this constraint, written in Zig specifically to exploit compile-time optimizations and zero-cost abstractions that higher-level languages simply cannot achieve.

Technical Insight

NullClaw's architecture centers on a vtable-based plugin system that achieves runtime polymorphism without the overhead of dynamic dispatch in languages like Python or JavaScript. Each component—providers, channels, tools, memory engines—implements a strict interface contract defined at compile time. Here's how a minimal provider plugin looks:

const Provider = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    const VTable = struct {
        complete: *const fn (*anyopaque, []const u8) Error![]const u8,
        stream: *const fn (*anyopaque, []const u8, StreamCallback) Error!void,
        deinit: *const fn (*anyopaque) void,
    };

    pub fn complete(self: Provider, prompt: []const u8) ![]const u8 {
        return self.vtable.complete(self.ptr, prompt);
    }
};

const OpenAIProvider = struct {
    allocator: Allocator,
    api_key: []const u8,
    endpoint: []const u8,

    pub fn asProvider(self: *OpenAIProvider) Provider {
        return Provider{
            .ptr = self,
            .vtable = &.{
                .complete = complete,
                .stream = stream,
                .deinit = deinit,
            },
        };
    }

    fn complete(ptr: *anyopaque, prompt: []const u8) ![]const u8 {
        const self = @ptrCast(*OpenAIProvider, @alignCast(@alignOf(OpenAIProvider), ptr));
        // Zero-allocation HTTP request using pre-allocated buffer pool
        return self.sendRequest(prompt);
    }
};

This pattern allows NullClaw to support 50+ different AI providers without runtime reflection or dynamic loading. At compile time, Zig inlines these vtable calls where possible, eliminating the indirection cost. The binary includes only the providers you actually configure, dead-code elimination strips unused implementations entirely.

The <2ms boot time comes from aggressive initialization deferral. Unlike frameworks that eagerly load configurations, validate schemas, and establish connections at startup, NullClaw does almost nothing until you send the first request. Configuration parsing uses Zig's comptime evaluation—JSON schemas are validated and transformed into efficient structs during compilation, not at runtime. Memory pools are pre-allocated but unfilled. Network connections lazy-initialize on first use.

Memory management is where Zig truly shines for this use case. NullClaw uses arena allocators extensively—entire request lifecycles allocate into temporary arenas that get freed in a single operation when the request completes. This eliminates per-allocation overhead and prevents fragmentation:

pub fn handleRequest(self: *Assistant, input: []const u8) !Response {
    var arena = std.heap.ArenaAllocator.init(self.base_allocator);
    defer arena.deinit(); // Entire request memory freed here

    const allocator = arena.allocator();
    
    // All intermediate allocations use arena - no individual frees needed
    const context = try self.memory.retrieve(allocator, input);
    const augmented = try self.augmentWithTools(allocator, input, context);
    const response = try self.provider.complete(augmented);
    
    // Only the final response is allocated from base allocator for persistence
    return try response.clone(self.base_allocator);
}

The 1MB peak RSS figure accounts for this arena strategy—temporary allocations spike and collapse within microseconds, while persistent state (conversation history, cached embeddings, tool definitions) stays compact through careful struct packing and string interning.

NullClaw's streaming implementation deserves special attention because it's where many frameworks leak memory. The assistant maintains a fixed-size ring buffer for streaming chunks, reusing the same memory across multiple streaming responses. When a provider sends Server-Sent Events, chunks are parsed directly into this buffer, callbacks fire synchronously, and buffer slots are immediately available for reuse. No dynamic allocation occurs in the hot path—the streaming loop runs entirely in stack-allocated memory after initialization.

The security model layers multiple isolation mechanisms. At the outermost layer, NullClaw can run inside Docker/Podman containers or systemd units with strict resource limits. The next layer uses Linux kernel features—landlock for filesystem access control, seccomp for syscall filtering. The workspace scoping layer prevents tools from accessing files outside designated directories. Finally, the pairing system ensures only authenticated clients can send commands, with secrets stored encrypted at rest using age encryption.

For developers extending NullClaw with custom tools, the plugin API provides a balance between safety and flexibility:

pub const Tool = struct {
    name: []const u8,
    description: []const u8,
    parameters: []const Parameter,
    execute_fn: *const fn (Context, Arguments) Error!Result,
    
    pub const Context = struct {
        allocator: Allocator,
        workspace: []const u8,  // Scoped directory access
        env: Environment,        // Sanitized environment variables
        max_duration_ms: u64,    // Timeout enforcement
    };
};

Tools receive a sandboxed context that prevents filesystem escape, limits execution time, and provides only explicitly allowed environment variables. This constraint-first design makes it harder to accidentally introduce security vulnerabilities compared to Python frameworks where tools have unrestricted system access by default.

Gotcha

The Zig 0.16.0 requirement is more painful than it sounds. Zig doesn't have stable releases yet—each version includes breaking changes, and your distribution's package manager almost certainly won't have exactly this version. You'll need to download the specific release tarball from ziglang.org, extract it manually, and manage PATH gymnastics. Nix users can work around this with a flake that pins the version, but for everyone else, expect friction. When Zig 0.17.0 arrives, NullClaw won't build until the maintainers update and potentially rewrite chunks of code for API changes.

The "fully autonomous" marketing claim oversells current reality. Yes, NullClaw supports tool calling, memory persistence, and multi-step reasoning, but the autonomous agent capabilities are rudimentary compared to mature Python frameworks. There's no built-in planning loop, no automatic goal decomposition, no sophisticated retry logic. You're getting infrastructure—channels, providers, tools, memory—not a turnkey autonomous agent. Building actual autonomous behavior requires significant custom code. The examples in the repository show simple request-response patterns and basic tool chains, not complex multi-agent scenarios or long-running autonomous tasks. For production autonomous agents, you'd still need to implement orchestration logic yourself.

Verdict

Use NullClaw if you're deploying AI assistants to resource-constrained edge devices (Raspberry Pi, industrial IoT, embedded Linux), building appliances where single-binary deployment dramatically simplifies operations, or have hard requirements on memory footprint and startup latency that disqualify Python/Node.js solutions. It's perfect for kiosks, robotics controllers, local-first applications, and scenarios where you'd rather ship one 678 KB binary than wrangle Docker images and dependency graphs. Skip it if you're deploying on conventional servers or developer workstations where resource constraints don't matter, need the mature ecosystem and extensive integrations of Langchain/LlamaIndex, want to iterate quickly without wrestling with Zig's learning curve and toolchain instability, or require production-proven autonomous agent patterns that go beyond basic tool calling. NullClaw solves a real problem brilliantly for the embedded/edge niche but represents over-engineering for typical cloud deployments where memory and startup time are non-issues.