# zclaw: an ESP32 assistant written in C under a hard 888 KiB flash budget

> zclaw puts a chat assistant, scheduler, GPIO control and persistent memory on an ESP32 and refuses to exceed a fixed all-in firmware size. The interesting part is what that cap forces the author to give up.

**tnm/zclaw** — Your personal AI assistant at all-in 888KiB (~35KB in app code). Running on an ESP32. GPIO, cron, custom tools, memory, and more.

- Repository: https://github.com/tnm/zclaw
- Website: https://zclaw.dev
- Stars: 2,233 · Forks: 193
- Language: C
- License: MIT
- Published: 2026-10-07 · Updated: 2026-10-07 · Language: en
- Canonical page: https://hysenlabs.com/projects/tnm-zclaw

## Fitting an assistant inside an 888 KiB firmware budget

The premise is unusual enough to be the whole pitch: zclaw is a personal AI assistant written in C for the ESP32, and it carries a strict all-in firmware budget of 888 KiB on the default build. The author is explicit that the cap covers everything, not just application code. It includes the zclaw logic plus the ESP-IDF and FreeRTOS runtime, Wi-Fi and networking, TLS and crypto, and the certificate bundle.

That distinction matters more than it sounds. A 888 KiB ceiling on your own code is comfortable; the same ceiling on a TLS client, a Wi-Fi stack and a certificate bundle is not. Every kilobyte spent on an assistant feature is a kilobyte not spent on the radio stack, which is why a release note can end up talking about trimming outbound TLS features to preserve headroom. The project describes itself as "Fun to use, fun to hack on", and the budget is what keeps the hacking part honest.

The hardware list is broad for something this constrained: ESP32, ESP32-C3, ESP32-S3 and ESP32-C6 are named as tested targets, classic ESP32-WROOM and DevKit boards are supported, and a Seeed XIAO ESP32-C3 is the recommended starter board. The repository carries separate defaults files for several of them, including `sdkconfig.esp32s3-box-3.defaults` and a QEMU profile, plus `partitions.csv`, which is how you get a two-stage OTA layout inside a tight budget.

The last push was on 2026-05-17, and the three releases that appear in the project's history are all from March 2026, which is the sort of pattern you see in a project where the author ships when there is something real to fix rather than on a calendar.

## Bootstrapping the toolchain and provisioning credentials after flashing

Installation is a single curl line on macOS and Linux. The bootstrap script clones or updates the repository and then runs `install.sh`, and the setup notes say you can inspect that flow before trusting it, including `ZCLAW_BOOTSTRAP_SHA256` integrity checks.

```bash
bash <(curl -fsSL https://raw.githubusercontent.com/tnm/zclaw/main/scripts/bootstrap.sh)
```

If you already have a clone, the same work happens through the local script. The non-interactive form is what CI or a scripted setup would use:

```bash
./install.sh -y
```

Credentials are a separate step from flashing, and that separation is a real design decision rather than an inconvenience. `./scripts/provision.sh` writes Wi-Fi and LLM credentials into NVS after the fact, so you can re-run it at any time without reflashing to change the SSID, the password, the backend, the model, the API key, or the Telegram token and chat ID allowlist. For encrypted credentials held in flash there is a secure mode, reachable through the install flow or directly with `./scripts/flash-secure.sh`.

Linux dependency installation auto-detects `apt-get`, `pacman`, `dnf` or `zypper` while `install.sh` runs, and in non-interactive mode any unanswered prompt defaults to no unless you pass `-y`. Default LLM rate limits are 100 per hour and 1000 per day, compiled in from `main/config.h` under `RATELIMIT_*`, so changing them is a rebuild rather than a setting.

The supported providers are Anthropic, OpenAI, OpenRouter and Ollama with a custom endpoint. That last option is what makes the project usable on a LAN without an internet account, and it is also the reason the size table below is worth reading closely.

## Reading the size breakdown, where the 833 KiB actually goes

The README publishes a segment breakdown taken from `idf.py -B build size-components`, grouped by image bytes. It is the most useful thing in the documentation because it tells you where the constraint binds.

| Segment | Bytes | Size | Share |
| --- | ---: | ---: | ---: |
| zclaw app logic (`libmain.a`) | `39276` | ~38.4 KiB | ~4.6% |
| Wi-Fi + networking stack | `378624` | ~369.8 KiB | ~44.4% |
| TLS/crypto stack | `134923` | ~131.8 KiB | ~15.8% |
| cert bundle + app metadata | `98425` | ~96.1 KiB | ~11.5% |
| other ESP-IDF/runtime/drivers/libc | `201786` | ~197.1 KiB | ~23.7% |

So the assistant you interact with is under 5% of the image. The Wi-Fi stack alone is 378624 bytes, close to nine times the size of the application. TLS and the certificate bundle together add another 15.8% and 11.5%, which is the concrete reason a release would cut outbound TLS features before it cut anything user-facing.

The default build totals 853034 bytes, and the padded `zclaw.bin` is 853184 bytes, about 833.2 KiB, leaving 56128 bytes or roughly 54.8 KiB under the cap. The v2.13.0 release notes report a different figure for a different build: 850048 bytes, 830.12 KiB, verified by `./scripts/check-binary-size.sh --build-dir build --binary zclaw.bin --max-bytes 909312`, and 909312 bytes is exactly 888 KiB. Two honest numbers, from two builds. Treat the cap as the rule and the README figure as a snapshot of one build, then check your own build against `--max-bytes`.

One number in the repository description does not match the README. The project summary advertises roughly 35KB of app code, while the size table puts `libmain.a` at 39276 bytes, about 38.4 KiB. Both may be right for their own build or measurement method, and the difference is small enough that it does not change the picture. The table is the more specific source, so use it when you are reasoning about what fits.

## Hardware control from chat, and the guardrails around it

The feature list is where the assistant stops being a chat toy. GPIO, DHT and I2C control all ship, and the v2.13.0 notes added first-class `dht_read` for DHT11 and DHT22 sensors plus generic raw I2C tools: `i2c_scan`, `i2c_write`, `i2c_read` and `i2c_write_read`.

Alongside those sit the things that make a device that acts on the physical world survivable. Schedules are timezone-aware and come in three shapes, `daily`, `periodic` and one-shot `once`. Diagnostics run through `get_diagnostics` with quick, runtime, memory, rates, time and all scopes. Persona options cover `neutral`, `friendly`, `technical` and `witty`. Tools are built in or user-defined, and a genuinely new built-in capability means writing a C handler plus a registry entry.

The word the README uses for all of this is guardrails, and that is the part to press on. It does not say in the README what those guardrails are. Whether a GPIO write is rate limited, whether an I2C write is bounded by address or length, whether an assistant reply can call a tool without confirmation, all of that lives on the docs site rather than in the repository page. That is not unusual for this kind of project, but it does mean you should read the Build Your Own Tool documentation before adding a tool that can move something.

The tool schemas were also hardened in v2.13.0, specifically for zero-argument built-in tools, because OpenAI-compatible validators rejected them. If you self-host an OpenAI-compatible endpoint rather than calling a hosted API, that release note is the one to read: it tells you the project cares about strict schema validation rather than sending something most servers would tolerate.

## Recovering a board with no network through the serial admin console

The most defensively designed part of the project is the one that assumes everything else has failed. When the board is in safe mode, unprovisioned, or the LLM path is unavailable, you can still operate it over USB serial without Wi-Fi and without an LLM round trip. `./scripts/monitor.sh` opens the console, and a small command set is typed directly:

```bash
./scripts/monitor.sh /dev/cu.usbmodem1101
```

```text
/wifi status
/wifi scan
/bootcount
/gpio all
/reboot
```

The full local-only set is `/gpio [all|pin|pin high|pin low]`, `/diag [scope] [verbose]`, `/reboot`, `/wifi [status|scan]`, `/bootcount` and `/factory-reset confirm`, which is destructive and wipes NVS before rebooting. A boot count is a small thing to expose and an extremely useful one when a board keeps rebooting for reasons the network path cannot tell you about.

For local work there is a repeat-provisioning path that avoids retyping secrets. `./scripts/provision-dev.sh --write-template` creates a profile, you edit `~/.config/zclaw/dev.env`, and `./scripts/provision-dev.sh --show-config` prints what it will use. If Telegram replays stale updates, `./scripts/telegram-clear-backlog.sh --show-config` shows the relevant configuration first.

The typical development loop is a short list of scripts rather than a build system you configure:

```bash
./scripts/test.sh host
./scripts/build.sh
./scripts/flash.sh --kill-monitor /dev/cu.usbmodem1101
./scripts/provision-dev.sh --port /dev/cu.usbmodem1101
```

Two details there are worth copying into your own habit. Host tests run without hardware, so you can iterate on logic before flashing. And `--kill-monitor` exists because a serial monitor holding the port is the most common reason a flash script appears to hang.

## What the version history reveals about the real constraints

The releases are few and unusually informative. v2.13.0, published 2026-03-22, is the most useful because every item in it maps to the budget: new DHT and raw I2C tools added, outbound TLS features trimmed to stay under 888 KiB, zero-argument tool schemas hardened for OpenAI-compatible validators, and a benchmark counter sequencing fix so serial soaks do not trip replay suppression across warmup and measured phases.

v2.11.2, from 2026-03-07, is a smaller fix set with one entry that matters on hardware: a fix for classic ESP32 HTTPS contention. The same release adds a model selection prompt and a curated model selection menu to the provisioning flow, plus a sidebar fix for reduced motion. v2.10.1, from 2026-03-03, hardened edge cases and added host tests.

Put together, that history says the project spends its effort where constraints bite: flash size, TLS behaviour on the older chip, validator compatibility with hosted APIs, and a benchmark harness that can produce trustworthy numbers. It does not say anything about a plugin system, a protocol, or a migration story.

Licensing is MIT, which means the C source is the part you can build on, and the custom tool path means extending it does not require upstream changes. Licence is worth reading next to the budget, though: an assistant that runs on your hardware and talks to hosted model APIs inherits those providers' terms regardless of what this repository grants. The README also links a changelog page and a complete README on the docs site, so the repository page is the summary and zclaw.dev is the manual.

## Conclusion

zclaw is worth a look if you want one board to answer Telegram messages, schedule its own tasks and toggle a GPIO, without giving a microcontroller root on your network. The 888 KiB budget is the real design document: it decides which TLS features survive a release, and the v2.13.0 notes show exactly that trade being made. What the documentation does not settle is how the assistant behaves once you enable several providers, the web relay and encrypted flash at once, because those paths are described only as separate options. Start with `./scripts/test.sh host`, then `./scripts/web-relay.sh` and one plain chat message before you wire anything to a pin.

## FAQ

### What ESP32 boards does zclaw support?

The README names ESP32, ESP32-C3, ESP32-S3 and ESP32-C6 as tested targets, and says classic ESP32-WROOM and ESP32 DevKit boards are supported. It recommends the Seeed XIAO ESP32-C3 as a starter board and asks for test reports on other variants.

### How much flash space does zclaw use on a default build?

The README reports a total image of 853034 bytes with a padded zclaw.bin of 853184 bytes, about 833.2 KiB, which leaves 56128 bytes under the 888 KiB cap. The application itself, libmain.a, is 39276 bytes, roughly 4.6% of the image, with the Wi-Fi stack taking the largest single share.

### How do I configure an ESP32 assistant when WiFi is not working yet?

Use the local admin console over USB serial, which works without Wi-Fi and without an LLM round trip. Run ./scripts/monitor.sh and type commands such as /wifi status, /wifi scan, /bootcount, /gpio all and /reboot. It is available in safe mode, when the board is unprovisioned, or when the LLM path is unavailable.

### Which LLM providers can zclaw talk to?

The README lists Anthropic, OpenAI, OpenRouter and Ollama, with Ollama taking a custom endpoint URL. Default rate limits are 100 requests per hour and 1000 per day, and they are compile-time settings in main/config.h under the RATELIMIT_ prefix rather than runtime settings.

## Sources

- [License: MIT](https://github.com/tnm/zclaw/blob/main/LICENSE)
- [Project website](https://zclaw.dev)
- [README](https://github.com/tnm/zclaw/blob/main/README.md)
- [Releases](https://github.com/tnm/zclaw/releases)
- [tnm/zclaw on GitHub](https://github.com/tnm/zclaw)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tnm-zclaw
