Codex vs Claude

Local Execution: It Worked in the Session Is the New Works on My Machine

David Guzenburg/ / 10 min read

The transcript says the tests passed. It does not say which Node version ran them, or that a shell function stopped the suite at the first failure.

reproducibilityshell accesstestingdeveloper experience

The security question has an answer; the correctness question does not

An agent running in your own terminal reads your files, runs your toolchain, and touches your services. The security consequences of that are well covered — the machine is the boundary, and shell execution blast radius works through what that means and how to contain it.

This article is about the other half, which gets far less attention and causes more day-to-day trouble: local execution makes results depend on the machine they were produced on. "It worked in the session" is the new "works on my machine", and it is harder to debug, because the session is gone and nobody recorded what the environment looked like while it ran.

What ambient means here

Every command an agent runs resolves against state nobody passed to it. The PATH that decides which python answers. The virtual environment that happens to be active. Environment variables set by a shell profile three years ago. A .env file loaded automatically by a directory hook. Version managers that switch the toolchain based on the directory. A cloud CLI already authenticated against a particular project.

None of that is visible in the transcript. The transcript says npm test and all 214 passing, and both are true, and neither tells you which Node version ran them.

The failure looks like flakiness

The characteristic shape: an agent makes a change, runs the test suite, sees green, and reports success. CI then fails on something unrelated-looking — a date formatting difference, a missing native dependency, a test that assumes a locale. Everyone assumes CI is flaky. It is not; it is the only participant with a declared environment.

The reason this is worth naming is that the natural response is to distrust the agent's work in general, which is both demoralising and wrong. The work was fine. The verification ran somewhere undeclared.

Shell configuration is instruction surface

This one surprises people. Your shell's aliases and functions rewrite commands before they execute, so an agent that runs git may not be running git.

# Innocuous personal conveniences that alter agent behaviour:
alias git='git --no-pager'          # changes output the agent parses
alias rm='rm -i'                    # prompts, and the agent has no tty
alias python=python3.11             # not the project's interpreter

pytest() { command pytest -x "$@"; }   # stops at first failure, so the
                                        # agent sees one error, not twelve

The pytest wrapper is my favourite example because it is actively helpful for a human and actively misleading for an agent: the agent concludes there is one failing test, fixes it, re-runs, finds another, and works through twelve failures one round trip at a time instead of seeing them together.

Check what the agent's shell actually is

Have it run type -a git python pytest npm and command -v on your main tools at the start of a session that matters. Thirty seconds, and it settles whether the commands in the transcript mean what they say.

Interactive prompts have no answer

A command that waits for confirmation in an agent session either hangs until a timeout or receives whatever the harness sends. Both outcomes are bad, and the second is worse because it looks like it worked.

The general fix is to make non-interactivity explicit rather than accidental: CI=true, DEBIAN_FRONTEND=noninteractive, --yes flags where the semantics are safe, and a GIT_TERMINAL_PROMPT=0 so a credential prompt fails fast instead of hanging. Set them in the session's environment rather than hoping every command is invoked correctly.

State accumulates across a long session

A container run starts clean every time. A local session does not. Two hours in, the working tree has scratch files, the database has rows from four test runs, a background process from a debugging detour is still holding a port, a global package got installed to make one thing work, and the branch is three commits into something abandoned.

None of that is a bug and all of it changes behaviour. The agent's model of the repository is also now partly wrong, because it has been reasoning from what it wrote rather than what is on disk. The cheap correction is a periodic git status and a look at what is listening, not because either is interesting but because divergence between the assumed and actual state is what produces confidently wrong next steps.

Declare the environment where the work happens

The whole of the fix is one idea: move the environment from the machine into the repository, so it travels with the code and can be asserted rather than assumed.

A pinned toolchain version in a file the version manager reads. A lockfile that is actually respected — npm ci rather than npm install, uv sync --frozen rather than a bare install. Explicit locale and timezone for anything that formats. A single documented command that runs the tests the way CI runs them, so there is one answer rather than a per-developer convention.

#!/usr/bin/env bash
# Print the environment a result was produced in. Cheap, and it turns
# "it worked in the session" into something checkable.
{
  echo "date:      $(date -u +%FT%TZ)"
  echo "host:      $(uname -srm)"
  echo "shell:     $SHELL"
  echo "cwd:       $PWD"
  echo "git:       $(git rev-parse --short HEAD) on $(git rev-parse --abbrev-ref HEAD)"
  echo "dirty:     $(git status --porcelain | wc -l | tr -d ' ') files"
  echo "node:      $(command -v node) $(node --version 2>/dev/null)"
  echo "python:    $(command -v python) $(python --version 2>&1)"
  echo "venv:      ${VIRTUAL_ENV:-none}"
  echo "TZ/LANG:   ${TZ:-unset} / ${LANG:-unset}"
  echo "aliases:   $(alias | wc -l | tr -d ' ') defined"
} | tee .last-env-report

Have the agent run it before a test run whose result will be reported to anyone else. The output is twelve lines and it converts a claim into evidence.

Reproducible enough, not reproducible

Full reproducibility is a container, and containers cost you the thing local execution was for: your running services, your authenticated CLIs, your editor state, your actual database. That trade is examined in sandboxing agent environments, and for a lot of day-to-day work the container is the wrong side of it.

The middle position is to make the parts that affect results declared, and leave the rest ambient. Toolchain versions, dependency resolution, locale and the test command: declared. Which terminal emulator you use, where your editor puts swap files, your shell prompt: ambient, and irrelevant.

The one-command rule

If there is a single habit worth adopting from this article, it is that every verifiable claim should correspond to one command that anyone can run. Not a sequence the agent worked out, not "run the tests but skip the slow ones", not a flag someone remembers. One command, in the repository, used by the agent and by CI.

That single change removes most of the divergence, because the agent stops inventing invocations. It is also the thing that makes an agent's verification worth anything: a result produced by the same command the pipeline runs is evidence about the pipeline, and a result produced by a bespoke invocation is evidence about that invocation. The general argument is in build and test instructions for agents, and reproducibility is the reason it pays off.

Where local execution is straightforwardly better

None of this is an argument against working locally. Debugging something that only reproduces against your real database. Iterating with a dev server already running and warm. Anything involving hardware, a VPN, an internal service, or a dataset too large to sync. Work where the feedback loop is seconds and a container round trip would be minutes.

In those cases the ambient environment is exactly the asset. The point is to know which of your results depend on it, so that the ones you hand to other people do not.

What to change on a normal laptop

Pin the toolchain in the repository. Add the one test command. Put CI=true and a couple of non-interactive variables in the agent's environment. Check type -a on your main tools once, and move any alias that rewrites behaviour into a differently named function. Have the agent print the environment report before reporting a result to anyone else.

That is an hour of work, it does not restrict what the agent can do, and it converts the most common false-confidence failure — green locally, red in CI, blamed on flakiness — into something with a visible cause.

The agent's memory of the repository goes stale too

There is a second kind of drift in a long local session, and it is between the agent's model of the code and the code. The agent read config.py ninety minutes ago. Since then you edited it in your editor, a rebase moved three files, and a dependency update rewrote a lockfile. None of that entered the session.

The symptom is a confidently wrong edit: a change applied to a function signature that no longer exists, or a patch that conflicts with something the agent has no idea happened. It reads as the agent being unreliable, and it is really an out-of-date snapshot being treated as current.

The habit that fixes it is cheap: before a substantial edit, re-read the file rather than working from what the session remembers, and check git status and git log --oneline -3 at the start of any resumed session. Both are one line. Neither is interesting until the session it saves.

Background processes are the ones that bite later

An agent that starts a dev server, a watcher, a tunnel or a database container during a session frequently does not stop it. The process outlives the session, holds a port, keeps a file lock, and continues writing to a log until something else fails for a reason that looks unrelated.

The classic version is a stale server on the port your tests expect. The next run passes against the old build, and the agent reports that its change works. That is a false green produced entirely by leftover state, and it is essentially undiagnosable from the transcript.

Ask for backgrounded processes to be started with an explicit way to stop them, and check for strays at the end of a session — lsof -i -P -n | grep LISTEN on a Mac, or the equivalent, takes a second and occasionally explains a whole afternoon.

Two machines, two answers

The clearest demonstration of the whole problem: give the same task to the same agent on two developers' laptops and compare. Different toolchain versions resolve, different services are running, different aliases apply, different credentials are live. The transcripts will look similar and the results will not always agree.

Teams discover this when someone says a task "doesn't work for me" and nobody can reproduce the discrepancy, because reproducing it means comparing two undeclared environments. The environment report from earlier turns that comparison into a diff of twelve lines, which is a solvable problem rather than an argument about whose setup is wrong.

What this means for trusting an agent's verification

The underlying point is about what a green result is evidence for. An agent running the test suite in a declared environment produces evidence about the code. The same agent running an ambiguous command in an undeclared environment produces evidence about that moment on that machine, which may or may not generalise.

Both look identical in the transcript — a command, some output, a summary. So the trustworthiness of an agent's self-verification is not a property of the agent at all; it is a property of how well the environment it ran in was pinned down. Teams that find agent verification unreliable have usually not made that distinction, and the fix is in the repository rather than in the prompt.

Takeaway

Local execution makes an agent's results depend on undeclared state: the resolved toolchain, an active virtual environment, aliases and shell functions that quietly rewrite commands, accumulated session leftovers. The symptom is green locally and red in CI, misread as flakiness. Move the parts that affect results into the repository — pinned versions, a respected lockfile, explicit locale, one canonical test command — leave the rest ambient, and have the agent print a twelve-line environment report before any result it reports to someone else.

Keep reading
Codex vs Claude

An Open Client and a Closed Model: Four Things the Licence Buys

A permissive licence on an agent CLI is worth reading the prompt assembly, auditing the sandbox code, embedding without procurement, and a fork you should probably not take. It buys nothing about the model service, its pricing or its terms.

ChatGPT vs Grok vs Claude Code

Code Answers, Sandboxed Execution and Repository Engineering

All three can write and explain code. Claude Code remains the specialist for multi-file repository work with commands and tests; ChatGPT can analyze.

Tooling & Integration

Sandboxing Agent Environments for Reproducibility, Not Just Safety

The operational case for containerising agent sessions: environment drift, parallel work and cleanup. A working setup, the mounts to refuse, and how to make the sandbox the easy path.

Codex vs Claude

Pairing With an Agent: Granularity Is the Whole Game

A check-in is valuable before a commitment and a tax during implementation. Where the real decision points are, the sentence that fixes ambiguous cases, and why the most useful conversation usually happens at the end.

← What Does the Entry-Level Plan Actually Buy?  ·  Below the Prompt: What a Kernel Sandbox Actually Constrains →

All codex vs claude articles  ·  Every article