Sh4d1/claw

Definitely the absolute smallest fully autonomous AI assistant, at least for today

★ 19Forks 0AssemblyGitHub ↗Compare

README

claw

A fully autonomous AI assistant in pure x86-64 assembly. No libc. No runtime. No sanity.

$ ./claw -m "Hello"
Thinking...
Hello! I'm claw, your AI assistant. How can I help you?

$ file claw
claw: ELF 64-bit LSB executable, x86-64, statically linked, stripped

$ ldd claw
	not a dynamic executable

$ wc -c claw
12056 claw

Why

noclaw ended its README with:

Until someone does one in asm I guess.

So here we are. 4,645 lines of hand-written NASM. Every syscall is a syscall instruction. Every byte of the binary was argued about. The JSON parser does recursive descent. In assembly. I need help.

What it does

Everything noclaw does, but with ~7x less binary and ~100x less self-respect:

  • Interactive mode with claw> prompt and conversation history
  • Single message mode with claw -m "do the thing"
  • Tool calling via the Anthropic Messages API (shell execution with stdout/stderr capture)
  • Agent loop that keeps going until Claude is done (or 10 iterations, whichever comes first)
  • Config from env vars (ANTHROPIC_API_KEY) or ~/.claw/config
  • TLS by shelling out to openssl s_client because implementing TLS in assembly is where I drew the line

The numbers

OpenClaw NanoBot PicoClaw ZeroClaw NullClaw NoClaw claw
Language TypeScript Python Go Rust Zig C x86-64 asm
RAM > 1 GB > 100 MB < 10 MB < 5 MB ~1 MB* 324 KB 384 KB**
Startup > 500 s > 30 s < 1 s < 10 ms < 8 ms idk man ~320 μs
Binary Size ~28 MB (dist) 363 MB (installed) ~8 MB 3.4 MB 678 KB 88 KB*** 11.7 KB
Build Deps node, pnpm, 1,219 pkgs pip, 103 pkgs go, 82 modules rustc, cargo, 737 crates zig, libsqlite3 cc, libbearssl nasm, ld
Runtime Deps node + npm python + pip libc libc libc + curl 0 openssl****
Source Files ~400+ ~54 ~129 ~120 ~110 ~14 16
libc V8 CPython go runtime musl zig std musl no
Cost Mac Mini $599 Linux SBC ~$50 Linux Board $10 Any $10 hardware Any $5 hardware Any 50¢ hardware It will probably only run on my laptop anyway...

Benchmarks measured on x86-64 Linux (Arch). RAM is peak RSS via GNU time. noclaw numbers from their README (arm64 Debian).

*NullClaw's ~1 MB RAM excludes TLS: it shells out to curl for every HTTP request, so TLS memory is charged to the child process.

**claw's 384 KB is the process itself. During API calls, peak RSS hits ~10.9 MB - but ~98% of that is the openssl child process. Same trick as NullClaw, honestly, but at least we admit it.

***noclaw: 88 KB dynamic (macOS arm64), ~270 KB static musl. claw: 11.7 KB, statically linked, no libc.

****openssl is only needed at runtime for TLS. It is not linked into the binary. The binary itself has zero dependencies. ldd says "not a dynamic executable" and it means it.

Code size breakdown

 9,628 bytes  .text    (executable code)
 1,947 bytes  .rodata  (strings, constants)
     0 bytes  .data    (no initialized mutable data)
88,200 bytes  .bss     (buffers, arena state - not in binary)
─────────────
12,056 bytes  total on disk (--nmagic packs text+rodata, no page padding)

Architecture

main.asm          _start, argv parsing, interactive loop
  config.asm      env vars, ~/.claw/config
  agent.asm       conversation history, tool dispatch, agent loop
    provider.asm  Anthropic API request/response serialization
      json_write.asm   buffer-based JSON builder
      json_parse.asm   recursive-descent JSON parser
      http.asm         HTTP/1.1 POST + response parser
        tls.asm        fork/exec openssl s_client, pipe I/O
    tool_shell.asm     fork/exec /bin/sh -c, capture output
  arena.asm       mmap-based bump allocator
  str.asm         string ops, itoa/atoi, env lookup
  io.asm          read/write wrappers
  data.asm        all .rodata strings

Headers: syscalls.inc (syscall numbers), macros.inc (save_regs, write_str, etc.), structs.inc (json_node, arena, message, etc.)

Design decisions that keep me up at night

  • Arena allocator: One mmap(MAP_ANONYMOUS) call gives you 16KB. Bump pointer goes up. When it's full, chain another chunk. When the conversation resets, munmap everything. No free(). No fragmentation. No mercy.

  • JSON parser: Full recursive descent. Strings, numbers, bools, null, arrays, objects. Escape sequence decoding (\n, \t, \", etc.). All arena-allocated. ~660 lines of assembly to parse {"content":"hello"}. I could have used jq.

  • TLS: I am not implementing TLS in assembly. I refuse. fork(), pipe(), dup2(), execve("/usr/bin/openssl", ["openssl", "s_client", "-connect", "api.anthropic.com:443", "-quiet", "-ign_eof"]). Write HTTP to the pipe. Read response from the other pipe. It works. It's fine. Don't look at it.

  • HTTP: Hand-assembled POST /v1/messages HTTP/1.1\r\n byte by byte. Content-Length computed via itoa(). Response parsed by scanning for \r\n\r\n. We support exactly one endpoint.

  • No libc, no exceptions: Every function call is call/ret. Every string operation is rep movsb or a manual loop. printf doesn't exist. malloc doesn't exist. Error handling is "return 0 and hope the caller checks". System V AMD64 ABI throughout - arguments in rdi/rsi/rdx/rcx/r8/r9, callee saves rbx/r12-r15/rbp, and god help you if you put a value in rcx and then call a function.

Building

make

Requirements: nasm, ld (GNU binutils). That's it. If nasm isn't in your PATH:

make NASM=/usr/bin/nasm LD=/usr/bin/ld

For the stripped binary:

make stripped

Usage

# Set your API key (pick one)
export ANTHROPIC_API_KEY=sk-ant-...
# or
echo "api_key=sk-ant-..." > ~/.claw/config

# Single message
./claw -m "What's in /etc/hostname?"

# Interactive
./claw
claw> List files in the current directory
claw> /new    # new conversation
claw> /quit   # exit

Platform support

Linux x86-64. That's it. The syscall numbers are hardcoded. The struct layouts are hardcoded. The ABI is hardcoded. If you're on ARM, I'm sorry. If you're on macOS, I'm more sorry - your syscall numbers are different AND unstable.

What's missing

Compared to noclaw:

  • No multiple providers (Anthropic only, claude-sonnet-4-20250514)
  • No Telegram/Discord/Slack (CLI only, this is assembly)
  • No file tools (shell tool can do cat though)
  • No memory system (conversations reset on exit)
  • No \uXXXX Unicode decoding (emits ? - sorry emoji users)
  • No chunked transfer encoding (we send Connection: close and read until EOF)

Bugs I found so you don't have to

  • pipe() returns two 32-bit ints. Not 64-bit. mov rdi, [rsp] packs both fds into one register. Use mov edi, [rsp].
  • Direct syscalls return -errno on error, not -1. mmap returning -12 is not a valid pointer. Ask me how I know.
  • rcx is caller-saved. If you compute a string length, store it in rcx, call arena_alloc, and then use rcx as if nothing happened - you will get a pointer-sized integer as your "string length" and mmap will try to allocate 140TB of RAM.

License

MIT

Contributors

Sh4d1

Issues