Skip to content

Sh4d1/claw

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

About

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

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors