It drafts a plan.
You approve it. Then it writes code.

PounceCode is a native macOS coding agent with a hard stop built into the middle of it. Ask for something substantial and it does not start typing — it comes back with a numbered plan and waits. You edit the steps, drop the ones you did not want, mark the ones worth pausing at, and only then does it touch a file.

inventory.py — PounceCode qwen3-27b · 24/24 tools · ctx 12%
1 2 3 4 5 6 7 8 9 10 11 12
# you Add a CLI to this project: add, list and remove. Tests for each, then commit. # PounceCode — plan held for review 1. Read inventory.py and test_inventory.py — understand the shape 2. Add remove(conn, sku) to inventory.py 3. Write cli.py with add / list / remove [checkpoint] 4. Add a test per command 5. Run pytest, then commit [checkpoint] [ Approve ] [ Edit a step ] [ Drop a step ] [ Reject ]
brew tap pounceapps/tap && brew install --cask pouncecode

Why a plan step at all

A one-shot agent is a bet: you describe the job, it disappears for four minutes, and you find out what it decided afterwards — by reading a diff. The expensive mistakes are the ones made in the first thirty seconds, and that is exactly when nobody is looking.

The plan is a separate call

Planning does not happen mid-conversation. It is its own model call with a clean context, because a model asked to "write your plan" halfway through a turn ratifies whatever it already intended to do.

The gate is in the registry

While a plan is held, every write tool is refused at the point of execution — not by asking the model nicely in a prompt. A model that skips the task board and reaches straight for write_file still gets stopped.

Checkpoints, not just a start gun

Approving is not a blank cheque. Mark any step as a checkpoint and the run stops there for another look. Steps that delete, deploy, push or drop get marked for you.

How plan review works, in detail →

Thirty-nine tools, and it only loads what it needs

Files, edits, shell, git, background processes, databases, SSH, CSV import, image reading, web fetch, document export. Handing all of them to a model at once is how a small model ends up picking the wrong one, so PounceCode keeps most of them behind named groups and lets the model ask for a group when the work calls for it.

GroupWhat it coversLoaded
coreTask board, file read/write, edits, shell, git, host probingalways
documentsExport a finished artefact as Markdown, PDF, DOCX or HTMLalways
webSearch and page fetchon demand
processStart, check, stop and list background processeson demand
csvPreview a CSV, import it into SQLiteon demand
imageRead an image so the model can look at iton demand
databases · sshQuery databases and run on hosts you have configuredwhen set up

Every tool, its arguments and what it can touch →

You can see what it is spending

Most agents show you a spinner. PounceCode puts the numbers on the status bar and keeps them in the conversation history: tokens in and out, how much of the context window is gone, what fraction of the prompt came back as a cache hit, and what the turn cost.

status bar
1
/work/inventory claude-sonnet-5 4 turns 18,402 tokens 24/24 tools ctx:12% csh:71% $0.032

ctx: the real number

The context percentage is measured against the window the selected model actually has — read from the provider's API where it reports one, probed from the endpoint for local models, and falling back to a per-family default that the app labels with a ~ so you know it is an estimate. When it genuinely cannot tell, it shows a dash rather than a confident zero.

csh: cache hits

A well-shaped prompt gets most of itself back from the provider's cache, billed at a fraction of the input rate. csh is the share of this turn's input that hit cache, so you can see whether the conversation is still cheap to continue.

What is tracked, and how cost is worked out →

Your models, including the ones on your own hardware

Anthropic, OpenAI, Google Gemini and any OpenAI-compatible endpoint, each as a named profile you switch between mid-conversation. Or point it at Ollama and run the whole thing on your own machine, where the code never leaves the building and the token cost is electricity.

Native APIs when they exist

A gateway that speaks OpenAI's shape will work, but the native API usually reports more — real context windows, cache-hit counts, proper rate-limit headers. PounceCode detects and prefers the native route when one is available.

Local models are a first-class target

The plan/step loop was tuned until a 27B model running locally could carry a five-step feature end to end. Small models find the rough edges that a frontier model papers over, which is why they are the benchmark.

The full local-models guide →

Things it has built

These are real artefacts, produced from a single prompt and rendered here exactly as they came out — no touch-ups.

Title slide of a generated presentation reading 'We just shipped this with one prompt', over a dark gradient with confetti.
An auto-cycling HTML presentation. One prompt: a deck explaining what we had just built, professional but fun, opened when done. Six slides, embedded CSS and JS, keyboard navigation, one file.
Slide four of the generated deck, describing the plan, write, verify and open stages.
Slide 4 — the deck explaining its own build.
Closing slide of the generated deck.
Slide 6 — the close.

Install

brew tap pounceapps/tap && brew install --cask pouncecode

Ships the desktop app and the pounce command-line tool together. macOS on Apple silicon, signed and notarized. Also on GitHub releases.

zsh — a real session, verbatimqwen3-27b, local
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
$ pounce "Read inventory.py, then add a remove(conn, sku) function and a test for it in test_inventory.py. Run the tests." ● read_file inventory.py ⎿ 29 lines ● read_file test_inventory.py ⎿ 8 lines ● edit_file inventory.py new 219B ⎿ Replaced 1 occurrence (+9 lines) ● edit_file test_inventory.py new 324B ⎿ Replaced 1 occurrence (+11 lines) ● run_command pytest test_inventory.py -v ⎿ ===== test session starts ===== (+9 lines) Done. · inventory.py: added remove(conn, sku) — deletes the row by SKU and commits, with a doc comment in the project's style. · test_inventory.py: added test_remove — adds, removes, asserts empty. · Ran pytest: 2 passed.

That is the actual output of that command against a 27B model running locally — not a mock-up. The same engine, profiles and tools as the desktop app.

more ways to call it
1 2 3 4 5
$ pounce --profile local-27b "why is test_inventory failing?" $ git diff | pounce "review this" $ pounce --continue "now commit it" $ pounce --yes "..." # unattended: skips confirmations $ pounce --setup # add a provider