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.

PounceCode holding a seven-step plan for review: a 'needs review' badge, the line 'Waiting for you. it is 7 steps, and the working folder has uncommitted changes. Nothing has been started.', an Approve and start button, a Send back button, and the steps each with pause and drop controls.
A real plan, held: seven steps, the reason it was held, and nothing started. Every step can be paused at or dropped before you approve.
The same plan, approved and running — three minutes at 3×. Steps strike through as they finish, each leaving a note on what it actually did; the composer becomes a place to add a note that goes next, and Stop is there the whole time.
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.

A finished run: 7 of 7 steps struck through, above an answer reporting 42 of 42 existing tests and 18 of 18 new tests passing, and a verification suite created.
Every step ticked off with a note on what it did, and the answer that follows leads with the outcome rather than replaying the steps you just watched.
A run stopped part-way, with the banner 'Stopped with 2 of 7 steps unfinished. Ask it to carry on, or work the rest yourself — nothing is running.'
Stop ends the turn between steps and between tool calls. What it finished stays reported; what it did not is named, rather than left for you to work out from the transcript.

How plan review works, in detail →

Two kinds of conversation

A chat is a single conversation: saved, with its own history and its own folder for anything it writes. Code work belongs to a project — a set of conversations sharing one top-level folder, where that project's files live. The app opens on the Chats side, because code work needs a project and opening by demanding one is no way to start.

PounceCode open on the Chats side: a Chats and Code switch at the top of the sidebar, a New button, and an empty conversation whose composer bar shows a Chat chip and the model name.
Chats and Code are the shape of the app, not a filter over one list. The conversation is the fact; the switch reads it.
The project panel open on its Artifacts tab, listing fourteen files the project has produced with their sizes, including chess.test.js, verify-tests.js, test-captured-pieces.js, game.js and index.html.
One panel per project: its folder, its conversations, its instructions, and everything it has produced — including the move-history panel and its tests, written while these screenshots were being taken.

The model belongs to the conversation

Not to the app, and not to whatever was selected last. It is named on the composer bar beside the project and chosen there, and the choice is written to that conversation — so reopening yesterday's work answers on the model it was held on. A local model and a hosted one can sit in two tabs of the same afternoon.

The model picker open above the composer bar, listing claude-haiku-4-5 from the hosted-flagship profile, qwen3-27b from local-27b, qwen2.5-coder:7b from workstation-ollama and qwen3.8-27b from gpu-node-01, with a Plans with: this model row at the foot.
Each model with the profile it comes from. Picking one writes it to this conversation and nothing else. The row at the foot points planning at a different model — plan with a strong one, execute with a cheap one.

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
The tool groups popover listing batch, csv, git, image, process and web with the number of tools in each; git is ticked as loaded.
The groups, with what each holds. Ticked ones are loaded; the model can pull in the rest when the work calls for them.

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.

The tokens and cost panel: 56,031 input, 1,799 output, 57,830 total; cache hits and cache writes on their own lines with an 81% hit-rate bar and what caching saved; the per-1K rates; and cost split into input, output and total, all to four decimal places.
Input and output, cache reads and cache writes counted apart — a write is billed at a premium, a read at a tenth — the rates being charged, and what the conversation has cost. Three turns on a cheap hosted model: $0.0268, an 81% cache hit rate, and the rows reconcile to the cent.

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 →

Made to be read for eight hours

Legibility is treated as a defect class here, not a preference. A font that renders thin, a label that clips mid-word, a colour that only clears contrast on paper — each of those is a bug with a test, and they get measured rather than eyeballed. Contrast ratios are computed; screenshots are sampled pixel by pixel to check what actually shipped.

The Appearance settings page showing every theme as a preview card in its own colours: twenty-two family themes including Rider Dark, Dracula, Tokyo Night and Terminal Green, then OLED, three corporate palettes, and two high-contrast ones.
Every theme previewed in its own colours, so you choose by looking rather than by name. Twenty-two of them, plus corporate and high-contrast sets.
The Fonts tab: a base size control, a text weight control set to Normal, a ligatures control, and two columns of typeface specimens for the interface font and the code font, each rendered in the face it names.
Base size, and weight beside it — past a point a bigger thin face is still a thin face, because the strokes grow with it. Every specimen renders in the face it names; the ones your machine does not have say so.

⌘+ and ⌘− move the whole interface, from anywhere. A size control you can only reach by reading the small text is not a size control.

A create_file tool call expanded in the transcript: a DONE badge, the path and byte count, and the added lines numbered and syntax-coloured — keywords, the function name, a docstring and comments each in their own colour.
A file write, syntax-coloured in the palette of whatever theme you are wearing — the tool reports the language it wrote, and the transcript reads it like code rather than like output.

0.8.0 — a terminal of its own

Running what it built used to mean opening Terminal.app: a window with the system’s font and the system’s colours rather than the ones you chose here, landing wherever the window manager put it. There is now one terminal inside PounceCode, in the working folder, in your theme and at your text size.

The terminal filling the work area of PounceCode: a header showing the folder /Users/Shared/pouncecode-demo/chess and a Close button, then a shell prompt reading chess %, the project’s files listed, and the output of its test run — suites 0, pass 65, fail 0, cancelled 0, skipped 0, todo 0. The sidebar and the composer are still there, and the Run-it bar below shows npm run dev.
The project’s own tests, run in the project’s own folder, without leaving the app. One terminal — not one per project and not tabs: a second terminal is a second place for state to hide. It takes over the work area rather than squeezing into a drawer, and closing it leaves the shell running with the scrollback you left in it.

It follows the project when it safely can. You cannot change a live process’s working directory — so at a prompt it moves with you, and while something is running it says not in this project and offers the move as a button rather than interrupting what you started. Which of the two it is comes from the kernel, not a guess.

0.7.0 — it tells you how to run what it built

A coding agent that finishes a program and leaves you to work out what to type has done half the job. PounceCode reads the project — its manifests, its scripts, its entry points — and puts the command on screen, with a button that opens a terminal already in the right folder with the command running.

The Run-it bar under the transcript: the words RUN IT, the command npm run dev, a note that it opens in a browser, and three buttons - Run it, Terminal and Copy.
Read off the project, not guessed by the model: the command comes from package.json, and the bar says so. “Terminal” opens the terminal above, already in the work folder; “Run it” starts the command there. A library with nothing to start says that plainly instead of inventing a command.

The plan is also a board you can read — one column per state, every step in full, and the note each one left behind.

The plan board filling the window: the goal across the top, a count of seven of eight done and nought of six checks met, and four columns - To do, Doing, Blocked and Done. One step waits in To do; the Done column holds ticked cards, each with its full step text and the note it finished with.
Seven of eight done, one still waiting, and every step readable in full rather than clipped to a line. The counts beside the goal are the plan’s own: steps done, and acceptance criteria met.

And when a plan is reported finished over a failing check, the board says so — not just the model. The chess game on this site was built that way: an engine whose move generator matches the published perft counts, and a suite that caught every attempt to call it done early.

Things it has built

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

Three of them start to finish, unedited apart from the dead air where a model is thinking. The caption on each is the whole brief it was given.

Blackjack, from one paragraph. “Build a blackjack game in Go with doubling down, splitting pairs and insurance. Dealer stands on soft 17. Write tests, then make sure it runs.” Out comes a planned, written and tested Go package. 3m 17s on claude-haiku-4-5.
A games API with a front end. One brief asking for a Go service, a Swagger UI at /docs with no authentication, and a static page with cards drawn in CSS — all served from the same binary, so go run . is the whole instruction. 42 tests. 4m 25s.
The harder case: fixing, not writing. 95 tests with fifteen failing, where most of the work is deciding which of them are wrong — several of those test positions were themselves bad chess. Every change is checked by the project’s own test command, which is what catches a model rewriting a test instead of the code. Ends on ✓ Pounced for 3m 3s.
The chess game in a browser after 1.e4 e5 2.Nf3 Nc6 3.Bc4 Nf6 4.Nxe5 Nxe5 5.Bd5: the board with a bishop on d5, a status line reading Black to move and Black +2, captured-piece trays holding a black pawn and a white knight, and the move list in algebraic notation underneath.
Play it here. A complete chess game in plain JavaScript — every rule, including castling, en passant, promotion, the fifty-move rule and threefold repetition — with an alpha-beta engine, a draggable board and algebraic notation. No build step and no dependencies. Its move generator is checked against perft, the standard test: 20 moves at depth one from the start, 400 at depth two, 8,902 at depth three, and the published counts for four other standard positions.
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.

Current release: 0.8.5 — Backspace works in the terminal: a GUI app is launched without a TERM, so the shell never emitted the erase sequence and the character you deleted stayed on screen. Colour, arrow keys and history were degraded by the same omission.

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