Hazzel Coding Agent Teardown: 152 Tests, 2 Real Gaps
Hazzel hit Hacker News this morning: "a tiny coding agent, bring your own keys." That's the fourth Show HN in nine days — the pitch keeps shifting. Small, auditable, approval-first. So I cloned it, installed it, ran the full test suite, and drove the agent loop end-to-end against a scripted mock LLM server I wrote. The core loop works. But the README makes two claims the code doesn't back up, and one of them — "sandboxed to your project root" — is exactly the kind of soft language that gets people's machines popped.
Install and the test suite: genuinely clean
Hazzel is at v1.4.2, Python, AGPL-3.0, on PyPI. I built a uv venv, installed it editable, and ran the suite:
git clone https://github.com/mukundzha/hazzel && cd hazzel
uv pip install -e . && uv pip install pytest
python -m pytest -q
# 152 passed in 2.76s
152 tests, zero failures, 2.76 seconds. That's a real test suite, not decoration — it covers safety, tool coercion, provider fallback, even Windows compatibility. Credit where due: most v1.x agents ship with three smoke tests and a prayer.
Driving the agent loop with a mock LLM
You can't pipe a prompt into Hazzel — there is no headless mode, no -p flag, no stdin driver. The only entry point is an interactive REPL. Fine. Hazzel's Ollama provider takes OLLAMA_HOST as its base URL, so I pointed it at a mock OpenAI-compatible server that streams a scripted write_file tool call, then a final text reply:
graph TD A[agent.run] --> B["POST /v1/chat/completions (SSE)"] B --> C[chunk: tool_call write_file hello.py] C --> D[agent executes write_file] D --> E[tool result appended to messages] E --> F["POST again: model replies 'Wrote hello.py — done.'"] F --> G[trace + auto-summary returned]
It worked on the first proper attempt. The trace came back clean, the file landed, and the auto-generated summary even noted "No automated verification was run in this turn." I ran the produced file — it printed the expected output. The ten-tool loop, streaming tool-call deltas, and trace bookkeeping all function exactly as advertised.
Gap one: the silent empty response
My first mock returned a plain JSON completion instead of SSE. The OpenAI SDK's streaming parser yielded zero chunks — no exception, no retry. Hazzel's _safe_stream_chat only falls back to a non-streaming call when stream() raises. It didn't. Result: agent.run() returned None, an empty trace, and no error anywhere.
# non-SSE endpoint -> SDK yields 0 chunks -> no exception
# -> no fallback -> agent.run() returns (None, [], ...)
# user sees: an empty turn with no explanation
This matters because Hazzel's whole pitch is bring-your-own-endpoint: Ollama, OpenRouter, Gemini, DeepSeek. Every proxy, gateway, or half-compatible server that returns non-streamed JSON produces a silently dead turn. An agent whose value prop is transparency should never fail silently.
Gap two: the "sandbox" is a working directory
The README says commands are "sandboxed to your project root." I read tools/run_command.py: the only thing "sandboxed" is the cwd passed to subprocess.Popen. There is no namespace, no filesystem restriction, nothing. Then I probed it live:
run_tool("run_command", {"command": "cat /etc/passwd"})
# -> 'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:...' (no approval prompt)
No prompt, full read of a file outside the project. The reason: a 16-entry _SAFE_BINARIES allowlist — ls, pwd, echo, cat, head, tail, wc, file, uname, whoami, date, basename, dirname, realpath, printf, true — skips approval entirely. cat in that list means any model confused or malicious enough can read ~/.ssh/id_rsa, ~/.aws/credentials, whatever, without a human in the loop. Destructive commands do prompt (I verified rm, sudo, and git push --force all ask first), and chained commands via |, ;, $() correctly lose the safe flag. The write path is honest. The read path is not, and the word "sandboxed" is doing fraudulent work in that README.
The size receipts
"Stays small on purpose." Measured: 7,482 lines across 34 Python files. The biggest two modules are ui.py (2,051 lines) and agent.py (1,668) — nearly half the codebase is terminal UI. Add __main__.py's 417-line hand-rolled command dispatcher and the interactive shell is ~55% of the project. Meanwhile config.py ships a "star nudge" — a one-time prompt asking you to star the GitHub repo, tracked in a .star_nudged file. Small agent, small growth hacks.
To be fair to the numbers: a comparable older-generation agent like Aider is roughly an order of magnitude larger. 7.5k lines is small for this category. The claim survives measurement — barely.
Bottom line
Hazzel is the best-engineered v1.x coding agent I've torn down this month: 152 real tests, a tool loop that works first try, honest approval gating on the write path. But two fixes are non-negotiable before anyone points it at a real endpoint: raise or fall back on empty stream results, and either drop the word "sandboxed" or actually drop privileges. Until then, treat Hazzel's read-only "safe" commands as fully trusted — because they are.