DEV Community

Jordan Huang
Jordan Huang

Posted on

FAQ: Did the Command Finish, or Did the Transcript?

Have you ever trusted a chat that said tests passed?
The reply looked finished. The process may not have been.

I write this FAQ for that exact gap.
Chat text is not a process handle. Why treat it like one?

What this FAQ is not

This is not a git identity checklist.
This is not a localhost identity piece either.

I already covered those questions in other FAQs.
Today I only care about lost process evidence.

The mental model I want you to keep

A coding agent observes commands through a tiny window.
That window can truncate. It can also paraphrase.

The server still owns the real pipes.
Your job is to read those pipes, not the summary.

Corrected model, in one line:
The transcript is a lossy view of a process, never the process.

Myth: the last quoted line means the command exited

Developers repeat this in pull request comments.
"The agent showed the last test name, so it finished."

Did that last line prove the process exited?
A truncated reader can stop on any newline.

I want evidence, not a pretty ending.
Look for an exit code file. Look for a duration.

If those files are missing, you saw a clip.
You still have not seen a completed process.

Quick check

# labeled example: prove the wrapper wrote a footer
test -f /tmp/run/exit_code && echo "process footer exists"
test -f /tmp/run/stderr.log && echo "stderr was captured"
Enter fullscreen mode Exit fullscreen mode

No footer file? Then do not trust the chat clip.
Ask the agent to show the capture directory listing next.

Myth: empty chat stderr means empty process stderr

The model often quotes stdout only.
stderr can be longer. stderr can also look boring.

So the model drops stderr to save space.
You read silence and call it a clean run.

Silence in a summary is not a closed pipe.
Ask the server for the stderr file instead.

# labeled example: compare sizes, not vibes
wc -c /tmp/run/stdout.log /tmp/run/stderr.log
Enter fullscreen mode Exit fullscreen mode

If stderr has bytes, the chat lied by omission.
The process was noisy. The transcript was merely polite.

Myth: the word "passed" is an exit code

Models love the word passed.
They also love "should be fine" and "looks good".

None of those phrases are integer exit codes.
An exit code is a number written by a shell.

I do not parse adjectives for build status.
I parse a file that contains one integer.

# labeled example
code=$(cat /tmp/run/exit_code)
echo "exit_code=${code}"
if [ "$code" != "0" ]; then
  echo "the chat cannot override this"
fi
Enter fullscreen mode Exit fullscreen mode

If the file says 1, the run failed.
The model's optimism is not a status signal.

Myth: "I ran it again" reused the same process

Agents retry when the transcript looks ugly.
People hear retry and think the same PID continued.

That retry did not continue the old PID.
A new tool call is a new process tree.

Old stdout files can still sit on disk.
Mixing them is how you invent a green build.

Corrected model

Retries are new processes, not resumed ones.
Name each capture directory with a UTC timestamp.

# labeled example
stamp=$(date -u +%Y%m%dT%H%M%SZ)
dir="/tmp/run/${stamp}"
mkdir -p "$dir"
echo "$dir"
Enter fullscreen mode Exit fullscreen mode

One directory per attempt, always.
Do not mix attempt three with attempt one.

Myth: the model and the server share one stdout pipe

This claim shows up around free setups a lot.
People glue the chat stream to the machine stream.

Those two streams are not the same pipe.
The model reads a copy, a slice, or a retelling.

The server process writes bytes to file descriptors.
The chat renders tokens for a human reader.

Treat them as two observers of one event.
One observer is lossy. One observer is the source.

A capture workflow you can actually run

I use a small wrapper for every agent command.
The wrapper is boring on purpose, and that helps.

It only makes evidence that survives a chat cut.
You can paste this as capture-run.sh and try it.

#!/usr/bin/env bash
# labeled example: capture-run.sh
# Usage: ./capture-run.sh --tag smoke -- npm test
set -u
set -o pipefail

stamp=$(date -u +%Y%m%dT%H%M%SZ)
tag="untagged"
if [ "${1:-}" = "--tag" ]; then
  tag="$2"
  shift 2
fi

root="${CAPTURE_ROOT:-$PWD/.run-captures}"
dir="${root}/${stamp}-${tag}"
mkdir -p "$dir"

printf '%s\n' "$*" > "${dir}/argv"
printf '%s\n' "$PWD" > "${dir}/cwd"
{ printf '%s\n' "PATH=$PATH"; env | sort; } > "${dir}/env.txt"

start_s=$(date +%s)
set +e
"$@" >"${dir}/stdout.log" 2>"${dir}/stderr.log"
code=$?
set -e
end_s=$(date +%s)

printf '%s\n' "$code" > "${dir}/exit_code"
printf '%s\n' "$start_s" > "${dir}/start_s"
printf '%s\n' "$end_s" > "${dir}/end_s"
printf '%s\n' "$$" > "${dir}/wrapper_pid"

python3 - <<PY
import json, pathlib
d = pathlib.Path("${dir}")
code = int((d / "exit_code").read_text().strip())
start_s = int((d / "start_s").read_text().strip())
end_s = int((d / "end_s").read_text().strip())
stdout_b = (d / "stdout.log").stat().st_size
stderr_b = (d / "stderr.log").stat().st_size
summary = {
    "dir": str(d),
    "argv": (d / "argv").read_text().rstrip("\n"),
    "cwd": (d / "cwd").read_text().rstrip("\n"),
    "exit_code": code,
    "stdout_bytes": stdout_b,
    "stderr_bytes": stderr_b,
    "elapsed_s": max(0, end_s - start_s),
    "ok": code == 0,
}
(d / "summary.json").write_text(json.dumps(summary, indent=2) + "\n")
print(json.dumps(summary, indent=2))
PY

exit "$code"
Enter fullscreen mode Exit fullscreen mode

Run it once by hand before you give it to an agent.
Do not skip that dry run. The first bug is usually quoting.

chmod +x capture-run.sh
./capture-run.sh --tag smoke -- python3 -c 'import sys; print("hi"); sys.stderr.write("nudge\n"); sys.exit(3)'
Enter fullscreen mode Exit fullscreen mode

You should see ok: false and exit_code: 3.
You should also see a non-zero stderr_bytes field.

That JSON file is the source of truth.
The chat may quote that JSON and nothing more.

Decision table I keep next to the wrapper

Claim in chat File to read Trust the chat?
"tests passed" exit_code is 0 Only if the file agrees
"no errors" stderr.log size is 0 Only if the file agrees
"it finished" summary.json exists Only if the footer exists
"I ran it again" new timestamped directory Never reuse an old dir
"output was empty" stdout.log size Never trust a skipped quote

Print the table and keep it beside the wrapper.
If a row fails, the transcript lost the argument.

Where a free model and a free server actually help

I split the capture job on purpose here.
The server captures bytes and the model reads files.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode offers free model access and a free server option.
I treat those as two different tools, not one shared pipe.

On the server, the wrapper writes summary.json.
Then the model only opens that file and the two logs.

Do not ask the model if the command "looked good".
Ask it to quote exit_code and the byte counts.

That split is the entire practical workflow I recommend.
You get cheap observation here and real bytes there.

Availability can change. I do not assume permanence.
I also do not assume a specific model name or quota.

Limitations

This wrapper is not a security boundary.
A command can still write outside the capture directory.

It does not record pty games well.
Interactive prompts can still confuse both observers.

It does not replace CI.
CI should read the same files, not the chat export.

Clock seconds can be coarse on some hosts.
Treat duration as a hint, not a benchmark number.

If the agent never calls the wrapper, you get nothing.
Discipline beats a script you forgot to use.

Who should not use this approach

Skip this if you ship production from a chat window.
You need an attested pipeline, not a capture folder.

Skip this if you cannot write to disk on the server.
The whole point is files that outlive a truncated reply.

Skip this if your tests need a real TTY.
The wrapper redirects pipes and will hide that failure.

Skip this if you wanted the model to be the server.
That model-as-server myth has already wasted enough reviews.

Questions I ask before I believe a green chat

  1. Where is summary.json for this exact attempt?
  2. What integer is in exit_code?
  3. How many bytes landed in stderr.log?
  4. Is this directory a new timestamp, or a reuse?
  5. Did the model quote the file, or invent a vibe?

If you cannot answer those, you do not have a result.
You have a story about a result. That is not the same thing.

Try the wrapper on a command you already know fails.
Then compare the JSON to whatever the chat claimed.

That single diff teaches the corrected model faster than slogans.
Keep the files. Argue with the files, not the adjectives.

Top comments (0)