Skip to main content

A CLI coding agent that writes and runs code in a sandbox.

Project description

CLI Coding Agent

A command-line coding agent: an agentic loop with tool-calling that reads/writes files and runs shell commands until a task is done — in your own directory, asking before each change, or inside a Docker container. No agent framework — raw OpenAI-compatible API calls against Groq and a hand-rolled loop.

Benchmarked against Terminal-Bench.

Install

pipx install dietcode

Needs Python 3.11+. Docker is optional and only used by --sandbox.

pipx is recommended because it puts dietcode on your PATH and keeps its dependencies isolated. pip install --user dietcode works too, but on Windows you may then need to add %APPDATA%\Python\Python311\Scripts to PATH yourself.

dietcode doctor         # checks Python, PATH, Docker and credentials

Then log in once:

dietcode login          # pick a provider, paste a key (input is hidden)
dietcode auth           # check what is configured

The key goes into your OS keychain (Windows Credential Manager, macOS Keychain, Secret Service on Linux), falling back to a 0600 file at ~/.dietcode/credentials.json. It is never written into the project.

Provider Free tier Get a key
groq yes, generous console.groq.com/keys
gemini yes aistudio.google.com/apikey
openai no platform.openai.com

Any OpenAI-compatible endpoint works via --base-url (Ollama, vLLM, OpenRouter).

Now run it from anywhere:

cd ~/some-project
dietcode

It works in the directory you are standing in, and asks before anything that writes, deletes, or reaches outside it. Docker is not required.

Running from a checkout instead
git clone https://github.com/DevPatils/dietCode.git
cd dietCode
pip install -e ".[dev]"
python cli.py                         # same code as the installed command

A .env with GROQ_API_KEY=... also works when running from a checkout.

Troubleshootingdietcode doctor diagnoses all of these:

Symptom Cause
dietcode: command not found its Scripts/bin dir is not on PATH — use pipx
no credentials for ... run dietcode login
Docker errors with --sandbox Docker isn't running — drop the flag to work locally
files disappear after a run you used --sandbox without --mount

Two ways to run

What it does Safety
dietcode Works in the current directory Consent — asks before each change
dietcode --sandbox Runs inside a Docker container Containment — cannot reach anything you did not mount

The default asks. Read-only commands (ls, cat, git status) run without prompting; anything that writes, deletes, or reaches outside the directory stops first with [y] yes [a] always allow [n] no. --yes skips every prompt and is exactly as dangerous as it sounds.

The sandbox contains. --sandbox puts everything in a container capped at 2 GB memory, 2 CPUs and 512 PIDs. Add --mount ./dir to let files persist, or --no-network to cut it off entirely. Either flag implies --sandbox.

The difference matters: a container is a boundary, a prompt is a decision. A shell command can always cd .., so the default protects you by showing what is about to happen, not by making escape impossible. For code you do not trust, use --sandbox.

Usage

Run with no arguments for an interactive session in the current directory:

dietcode

One session keeps its conversation and its files — the agent remembers what it did on previous turns and the files it built are still there. Slash commands: /help, /files, /cost, /sandbox, /clear (forget the conversation, keep the files), /exit. Ctrl+C interrupts a turn without quitting.

Or pass a task to run once and exit — used for scripting and the benchmark:

dietcode "add a test for the parser and make it pass"

To run the same thing inside a container instead, add --sandbox. Note that a container is thrown away when the run ends, so mount a directory if you want the files to survive:

dietcode --sandbox --mount ./my-project "add a test for the parser"
Flag Meaning
--steps show step separators
--no-stream wait for each reply instead of showing it as it is generated
--subagents let the agent delegate self-contained work to sub-agents
--no-context ignore the project's DIETCODE.md / AGENTS.md
--provider groq|gemini|openai which API to use (default: your saved login)
--base-url URL any OpenAI-compatible endpoint (Ollama, vLLM, OpenRouter)
--no-network cut the sandbox off from the network entirely
--max-tokens N hard spend ceiling per task
--context-budget N trim the oldest turns above this prompt size (default 48000)
--memory / --cpus / --pids-limit container resource caps (default 2g / 2 / 512)
--cleanup remove every leftover agent container and exit
--sandbox run inside a Docker container instead of the current directory
--mount HOSTDIR[:TARGET] bind-mount a directory into the container. Implies --sandbox
--yes approve every action without asking (dangerous)
--container NAME attach to an existing container instead of creating one
--image IMAGE sandbox image (default python:3.11-slim)
--model NAME default llama-3.3-70b-versatile
--max-iterations N default 12
--json print metrics as JSON
--quiet only print the final result

Exit code is 0 when the agent called task_complete, 1 otherwise.

Tests

python -m pytest                       # Docker tests skip if the daemon is down
python -m pytest tests/test_loop.py     # one file
python -m pytest -k timeout             # one test

The loop tests use a scripted fake client (tests/fake_llm.py), so the suite needs no API key and makes no network calls.

How it works

interactive ─┐
one-shot   ──┼─> agent_loop ──> execute_tool ──> Executor ──> container
tb run     ──┘   (agent/loop.py)  (agent/tools.py)  (agent/sandbox.py)

All three entrypoints run the same loop. Interactive mode differs only in that it passes the previous turn's messages back in as history and reuses one container; rendering lives in agent/ui.py so the loop stays UI-free and the benchmark can run it with no console attached.

Replies stream token by token in both human-facing modes. agent_loop(stream=…) defaults to off, and the benchmark leaves it off deliberately: streaming means reassembling tool calls from fragments, which is strictly more machinery to go wrong, and a scored run gains nothing from output nobody watches. Both transports normalize to the same Completion, so the loop itself is identical either way.

agent_loop calls the model, executes whatever tools it asks for, feeds the results back, and repeats until task_complete, a turn with no tool calls, or max_iterations.

Tools: read_file, write_file, edit_file, find_files, search, run_shell, task_complete — plus spawn_subagent behind --subagents.

edit_file replaces an exact snippet rather than rewriting the file, so a one-line change costs one line instead of four hundred. It refuses rather than guesses: no match, or an ambiguous match, is an error explaining what to fix.

--subagents lets the agent delegate self-contained work to a fresh agent that shares the files but not the conversation, and reports back only a summary. The context isolation is the point — passing the transcript back would cost as much as doing the work inline.

Project instructions. If the working directory has a DIETCODE.md, AGENTS.md, CLAUDE.md or .cursorrules, it is appended to the system prompt and takes precedence over the defaults. Read from the host, so the agent can't rewrite its own standing orders. --no-context skips it.

The only thing that differs between the CLI and the benchmark is which Executor gets passed in, so both run identical tool code.

Notes from building it

  • execute_tool never raises. Llama and Qwen emit malformed tool-call JSON, invented tool names and wrong-typed arguments often enough that treating those as exceptions would kill a run several times per benchmark. Every failure comes back as an error string the model can read and correct.
  • Tool calls written as prose are recovered. On the very first real run, llama-3.3-70b emitted <function/run_shell {...}</function> as message text rather than through the tool-calling API. The loop saw no tool calls and stopped on step 1 with the task untouched. extract_tool_calls_from_text parses the known text formats, and the recovered call is rewritten into the transcript in correct structural form. Counted separately as recovered_tool_calls — it measures the model, not the scaffold.
  • File tools go through the executor, not the host filesystem. Otherwise the benchmark agent would read the host while its shell acts in the container.
  • task_complete batched with the work gets deferred. Models often emit write + run + task_complete in a single turn, declaring the output verified before a single tool result existed. One run wrote bash into a .py file, got a SyntaxError, and claimed success in the same breath — it would have scored a false pass. The loop now feeds the results back and requires task_complete on its own turn.
  • Schemas stay permissive where the dispatcher coerces. Groq validates tool arguments server-side and 400s the whole generation on a mismatch; a model sending "timeout": "10" killed a run. The rejected text comes back in failed_generation, so the call is salvaged from it rather than lost.
  • The shell wrapper persists the working directory between calls. Each docker exec is a fresh process, so cd /app in one command would be silently lost by the next.
  • Written file content is base64'd over argv, so nothing the model generates can be reinterpreted as shell syntax. Costs a ~1MB write ceiling (ARG_MAX).

Limits and isolation

The agent runs shell commands an LLM wrote, so containers are capped by default: 2 GB memory, 2 CPUs, 512 PIDs, plus no-new-privileges. The PID cap is what stops a fork bomb from wedging the Docker VM rather than just failing a command.

Networking is on by default (the agent often needs pip install). Use --no-network for untrusted work — note that combined with --mount, a networked agent can read your mounted files and send them somewhere.

Every container is labelled, and startup sweeps ones older than 6 hours left behind by a crash. --cleanup removes them all now. This matters because close() only runs on a clean exit — SIGKILL leaks a container otherwise.

Long sessions trim their own history: above --context-budget tokens the oldest turns are dropped, always keeping tool calls and their results together (splitting a pair makes the API reject the whole request). Trimming applies to what is sent; the full transcript is still recorded.

Benchmark

The harness does not run on Windows. Use WSL or Linux:

curl -LsSf https://astral.sh/uv/install.sh | sh   # uv brings its own Python 3.13
uv tool install terminal-bench

wsl bash scripts/benchmark.sh                 # hello-world
wsl bash scripts/benchmark.sh broken-python   # a single task
DATASET=terminal-bench-core==0.1.1 wsl bash scripts/benchmark.sh ""   # everything

Docker Desktop's WSL integration means WSL shares the same daemon — no second install. The agent itself is fine on Windows; only the harness is not.

Four terminal-bench 0.2.18 problems this works around
  1. Its dataset downloader shells out to Unix rm -rf .git. Needs Git's usr/bin on PATH, or just run it on Linux.
  2. terminal-bench-core@head points at ./tasks, but the repo moved to harbor-framework/terminal-bench and renamed that directory original-tasks/. Pin ==0.1.1 (commit 91e10457b5).
  3. Windows blocker: container paths are built with pathlib.Path, so /tmp becomes \tmp and the run dies in TmuxSession.__init__ with 404 Could not find the file \tmp — before the agent is ever called. The 0.00% this produces is not a score; check total_input_tokens: null in results.json to tell "harness failed" from "agent failed".
  4. It finishes by printing output_path.absolute(), which calls os.getcwd(). On a OneDrive-backed folder over WSL's drvfs that can throw after a successful run. Passing an absolute --output-path avoids the call.

tb does not read .env; the adapter loads it itself, and the script exports the key as well in case the harness's isolated environment lacks python-dotenv.

Use a fixed ~15–20 task subset while iterating — not the full suite, and not repeatedly. Groq's free tier is ~1,000 requests/day and each task burns one request per loop step.

Per-task metrics.json and transcript.json are written into the harness's logging directory; they are the input to the failure-mode table below.

Results

Smoke test only so far — one task, which is not a score.

Task Result Steps Tokens Notes
hello-world ✅ resolved 3 1,806 1 tool call recovered from text

The full subset run is the next step. The table below stays empty until then rather than extrapolating from a single task.

Resolution rate Avg steps Avg tokens
This agent
Terminus (reference)

What the first pass showed

Both defensive mechanisms earned their place immediately. From the transcript:

  • Step 1's tool call arrived as prose, not through the tool-calling API. The recovered call is visible in the log as a synthesized id (call_1_0) with empty content. Without extract_tool_calls_from_text the loop would have stopped at step 1, hello.txt would never have been written, and the task would have failed.
  • Step 2 sent "timeout": "30" as a string. That is exactly the payload that previously drew a 400 and killed a run; the permissive schema absorbed it.

One task on one model is a smoke test, so treat recovered_tool_calls as the interesting number here, not the pass.

Status

Built and working end to end: tool dispatch, agent loop, Docker sandbox, CLI, Terminal-Bench adapter.

First real run, "write a python script that prints the first 20 primes and run it" on llama-3.3-70b-versatile — completed in 4 steps / 4658 tokens:

Step What happened
1 Tool call arrived as text; recovered and run. The command itself was malformed (literal \n inside python -c "...") → SyntaxError
2 Model read the error, switched to write_file, and used a proper structured tool call
3 Ran the script; correct output
4 task_complete

Not yet done:

  • A benchmark run.
  • Stretch goal: spawn_subagent — a fresh loop with isolated message history that returns only its final summary to the parent. agent_loop takes an extra_tool_handlers hook for exactly this, and the hook is tested; the tool itself is deliberately left until there is a baseline score to compare against.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dietcode-0.3.0.tar.gz (94.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dietcode-0.3.0-py3-none-any.whl (66.5 kB view details)

Uploaded Python 3

File details

Details for the file dietcode-0.3.0.tar.gz.

File metadata

  • Download URL: dietcode-0.3.0.tar.gz
  • Upload date:
  • Size: 94.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for dietcode-0.3.0.tar.gz
Algorithm Hash digest
SHA256 a593bb8fad9c507b6cc2b62d6ce83ea60752e4adff3f8a9a03d7066b2f01f3a9
MD5 fd185f2ad12affbeb564c34873b4424f
BLAKE2b-256 2dc00357725df9bf25b06e38360a560123010de32c5b0067642b88840166c902

See more details on using hashes here.

Provenance

The following attestation bundles were made for dietcode-0.3.0.tar.gz:

Publisher: publish.yml on DevPatils/dietCode

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dietcode-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: dietcode-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 66.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for dietcode-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 926a29e339633a3941b96cba90aea6ace525b509236e00e1ca3bf2f08332a892
MD5 c61e2faf7e6e4370acd4a25ad3398af3
BLAKE2b-256 a70b81648b57feb02840d1ccfc4f70d5a99688aed35f1b48a5380282740c8019

See more details on using hashes here.

Provenance

The following attestation bundles were made for dietcode-0.3.0-py3-none-any.whl:

Publisher: publish.yml on DevPatils/dietCode

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page