Skip to content
mp.
All writing
Engineering

How I work with coding agents

Passing a local test, opening the simulator, and checking production answer different questions. Most of how I work with coding agents comes down to knowing which of those questions has actually been answered.

I use Claude Code most, and Codex alongside it, from investigating a bug to checking a release. I coordinate several conversations, let them exchange findings, and bring in another model to try to break a change. I also write code myself, and I’m particular about understanding why a change should work.

What is already there before I send the prompt

I use Claude Code through Supacode, with the conversation, terminal, and diff close together. My personal instructions include “I prompt short and let you run.” They also say to read the repository first, use its existing commands, preserve unrelated changes, and be honest about what was checked. General preferences live in the global instructions; project constraints live in the repository. Claude’s documentation explains how those are loaded.

Scope creep is the failure I catch most often, usually by looking at the diff size afterwards. So the instructions ask the agent to report the diff size, explain unexpected growth, and list adjacent issues separately instead of fixing them. The agent doesn’t ask before every file read, but it stops when a decision changes the scope.

For planning, review, and simplification I use Compound Engineering and Matt Pocock’s skills. A skill gives an agent a procedure when it needs one. I don’t run the full workflow for every edit.

Sometimes my next message is just:

make it better. make no mistakes.

That only works as a follow-up inside an existing task, where the repository, the earlier discussion, and the checks give “better” a meaning. When the requirement is still unclear, I have to explain the behaviour first.

Work out what must stay true

In my art gallery project, two requests can update different artworks in the same collection. The requirement is that both successful updates survive, whichever request finishes first. Once that is stated, the agent has a concrete job: trace where each callback gets its copy of the state.

If a callback replaces the collection with the snapshot it captured when the request started, it can erase the other update. That explains the failure and gives us a test: complete B before A, then check that both URLs remain. The test further down does exactly that.

So before an implementation, I identify the state, who can change it, and what has to remain true. A suggestion to add a cache or another abstraction has to explain how it addresses that specific failure. The same applies to the tools around the agent: a preview should leave the files it inspects unchanged, and a review should apply to the revision being shipped.

Let it use the app

For UI work, the agent opens the browser or simulator and tries the interaction. With browser tools or Argent, it can press the button, inspect the result, and repeat the same steps after a change. I ask for a recording when the problem involves movement or a sequence of interactions.

In the gallery, I kept asking whether reviewed translations had actually reached the website. Answering that meant comparing the supplied document, the current copy, and the deployed page. A successful build could not answer it.

During investigations I also use Sentry, PostHog, and Langfuse. With traces available, I ask for a breakdown before asking for a fix. A typical request, with project details removed:

Inspect the traces for this time window. Read only.
Break down p50 and p90 duration by step and identify the main contributors.
Distinguish what the trace shows from your hypothesis about the cause.
Bring back the evidence before proposing a change.

I also ask which version and backend the agent inspected. I’ve had enough “it works here” answers to want to know where “here” is.

Orchestrate the conversations

I often have several chats working at once. A lead conversation coordinates the task, sends work to another chat, and brings its findings back. Chats can also exchange questions and results through the available tools. A subagent is a delegated worker inside one session, with its own context and tool access; another chat has its own history and may run a different model. My setup has specialists for React Native state, performance, accessibility, and security, and I pick them for the question in front of me.

None of them automatically knows what happened elsewhere. A handoff needs the question, the relevant files, the current revision, the constraints, and what has already been checked. An exchange between chats is useful when it produces something concrete: the relevant code, the conditions under which it fails, and a check that tells two explanations apart. Two chats repeating each other’s conclusions is not an independent review.

Separate conversations don’t isolate files, so I use Git worktrees when implementations need separate working copies. Before combining results, I check which revision each worker read and what it changed. If the code moved during a review, an earlier finding may no longer apply, and the combined change still needs its own check.

Have another model try to break the change

Compound Engineering’s ce-code-review is an important part of my setup, mostly for its adversarial reviewer. It asks how a change fails when components interact, requests overlap, or recovery takes an unexpected path, and it should construct the sequence that breaks it. For a verification tool like uivet, the question is whether a wrong interface can still get a green result.

Its cross-model pass can route that review to a different model, so the implementation is checked by something other than its author reconsidering. I care which model actually performed the review. Opening a second chat in another app doesn’t establish that a different model served it. If the cross-model pass couldn’t run, that is a coverage limit to report, not a fallback to describe as an independent check.

Findings get checked against the code before I apply them. Two models agreeing is not proof, and a plausible failure story still needs a reachable path. Sometimes the right outcome is to reject the finding, remove an unnecessary patch, or leave the implementation alone.

How a task moves through my setup
Establish the taskRequest, instructions, current revision, lead conversation
Investigate independently
Trace the behaviourCode, browser, simulator, traces
Challenge the explanationSeparate chat or model, adversarial review, another hypothesis
Compare evidence
Make the changeSend findings back; resolve disagreements with evidence
Run checksTests and the actual browser or device
Review the diffCorrectness, scope, then simplification
Checks pass
Hand off verified workKeep useful tests, instructions, skills, and decisions for the next task.

If something fails: revise the change and rerun the relevant checks.

Chats exchange findings through tools and handoffs. Independent work still needs integration and verification; I do not start extra agents for every edit.

Give the check a chance to fail

Review is not enough on its own; I also need a check I can run. In uivet, I deliberately remove a price from an interface and check that the report names the missing value and the process fails. Checking only the exit code could mistake a startup error for a passing test.

Here is the gallery race from earlier as a standalone test. It models the state update without React, a server, or my repository. Save it as media-update.test.ts and run bun test ./media-update.test.ts with Bun.

import { expect, test } from 'bun:test';

test('a slower request preserves the result that arrived first', async () => {
  let artworks = [
    { id: 'a', url: '' },
    { id: 'b', url: '' },
  ];
  const a = Promise.withResolvers<string>();
  const b = Promise.withResolvers<string>();

  const pending = [a, b].map((request, index) =>
    request.promise.then(url => {
      artworks = artworks.map((artwork, position) =>
        position === index ? { ...artwork, url } : artwork
      );
    })
  );

  b.resolve('/b.webp');
  await pending[1];
  expect(artworks).toEqual([
    { id: 'a', url: '' },
    { id: 'b', url: '/b.webp' },
  ]);

  a.resolve('/a.webp');
  await pending[0];
  expect(artworks).toEqual([
    { id: 'a', url: '/a.webp' },
    { id: 'b', url: '/b.webp' },
  ]);
});

To see the bug, add const initial = artworks; before const pending, then replace artworks = artworks.map with artworks = initial.map. The last assertion fails because A’s callback brings back B’s empty URL. I ran both versions while writing this.

The test fixes the completion order explicitly instead of hoping one timeout beats another. It assumes a fixed collection and independent updates to different entries. It does not test React rendering, URL expiry, or unmounting; the gallery’s own tests cover those separately.

Use artifacts to understand a large change

I still read code. In a large repository, I want help finding the parts that deserve a close read. For that I use artifacts: an HTML walkthrough, a diagram of the request path, or a report with the results beside the change. Theo’s video about HTML artifacts is a reference for this part of my workflow.

Cursor’s pr-review-canvas skill fetches a pull request’s description, diffs, and review comments, and builds an HTML page with annotations beside the changes. Mechanical edits collapse; the important implementation stays open. For the media update, that means seeing which callback owns the state and why the old version loses an update, then reading the code with those questions in mind.

Open an example HTML review artifact. I made it for this article from the same race as the test above; it is not an export of a private PR.

An artifact can omit a failure path as easily as a chat response can, so it needs a route back to the source: the actual diff, the revision under review, and the checks. If the implementation changes after the artifact was generated, the review needs updating too.

Review the requirement and the implementation

I review correctness before simplification: failures in state handling, permissions, or platform behaviour come before looking for code to remove. Matt Pocock’s code-review skill separates the project’s standards from the originating requirement, and I like that distinction. A change can follow every convention and still build the wrong thing.

I often ask why a new helper, package, or configuration file was needed. Sometimes there is a good reason; sometimes the existing code already does the job. After a fix, the relevant check runs again, and I want to know which app or platform it covered. Then I follow the change to the version people use: whether it was pushed, whether the deployment finished, and whether the deployed behaviour matches what we tested.

Check whether the change is worth making

Some tasks should end with an investigation. When I haven’t decided whether a change is desirable, I ask for a recommendation before a branch. An agent having already built something is not a reason to ship it.

I also use agents to compare model outputs and investigate latency and cost. I push back when a recommendation rests on a small probe or an unexplained count, and ask for the inputs, what differed between the alternatives, and the query behind the measurement. A new model can be slower without being better. A clean refactor can expand the task without fixing the problem. Keeping what already works is a valid outcome.

The audience matters for that evidence too. A report for someone making a product decision should show the main comparison first and keep the technical detail underneath. I often have to push the first version toward that.

I keep the setup in Git too

A private agent-config repository holds the portable parts of Claude Code and Codex: instructions, settings, agent definitions, commands, and skills. Shared skills live in one directory so both tools can use them. The backup excludes conversations, memory, credentials, caches, and machine-specific state; plugins have reinstall manifests.

The sync only copies files I have already chosen to keep, and I preview the changes before committing. The backup process has its own tests. One changes a temporary copy of my instructions, runs the sync preview, and checks that the repository copy stayed unchanged. Another checks that restoring refuses to overwrite an existing settings file. A third makes sure the secret scan rejects a fake credential.

Keep what helps

When a session uncovers something worth keeping, I turn it into a focused test, an instruction, or a skill. For the gallery, that was a handoff another agent could check against the repository and the live site when the next email arrived. For my own setup, it became the config sync.

I want the next session to start with what we learned, and still check that those notes match the code.

All engineering, essays, and fiction.