Did your agent just declare the suite green?
Was that a process exit, or buffered output?
Remote agent boxes lie in boring, local ways.
They lie through pipes, pagers, and block buffers.
This post is not a rant about model quality.
It is a stdio FAQ you can run tonight.
Why this FAQ exists
I review agent logs the same way I review CI.
The chat transcript is not actual kernel evidence.
A free remote box has no TTY most nights.
Your laptop still has a pager and live prints.
Guess which environment the coding agent actually used?
That mismatch is where local green reports are born.
Where a free model and a free server fit
I run this canary on a throwaway box first.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode offers free model access and a free server option.
I treat both as a lab, never as production identity.
The workflow below does not need a paid GPU.
It needs a shell, Python, and one git checkout.
Skip the product if you already have a box.
Keep the canary, because these myths do not care.
Myth 1: The last log line is the exit code
Agents love to quote the final stdout line.
Human reviewers quote that line for the same reason.
It feels like a result, but it is not.
It is only a byte stream, not a verdict.
false
echo "tests passed"
echo "exit=$?"
That script prints a cheerful lie, then zero.
Nobody in the pipeline asked that failed process.
What is the corrected mental model here?
Read the status in the same shell immediately.
Better, write the code into the log yourself.
Do not let a later chat summary invent success.
rc=0
pytest -q || rc=$?
echo "CANARY_EXIT=$rc"
exit "$rc"
Myth 2: A pipe failure will scream at you
Default bash does not enable set -o pipefail.
The last process in the pipe owns $?.
set +o pipefail
cat missing.txt | wc -l
echo "exit=$?"
wc succeeded even though the file was missing.
Your agent may still report a legitimate zero count.
set -o pipefail
cat missing.txt | wc -l
echo "exit=$?"
Now the pipeline fails, which is what you wanted.
Did the agent export that option before the run?
Here is the corrected mental model for pipes.
A pipe is a process group under a policy flag.
You have to opt in for pipeline failure.
The wrapper must opt in on every agent run.
Myth 3: Python prints are live on a remote box
On a TTY, CPython line-buffers standard output.
On a pipe, CPython switches to block buffering.
Your free server is usually a pipe, not a TTY.
That single fact explains a lot of fake hangs.
cat > /tmp/prints.py <<'PY'
import time
print("start")
time.sleep(5)
print("done")
PY
python3 /tmp/prints.py
python3 /tmp/prints.py | cat
You wait five seconds, then both lines arrive together.
The agent screenshot now looks like a deadlock.
Fix it with -u or an environment variable.
PYTHONUNBUFFERED=1 python3 /tmp/prints.py | cat
python3 -u /tmp/prints.py | cat
The corrected model is simple: no TTY, delayed truth.
A hang can be a full buffer, not a deadlock.
Myth 4: If color showed, the command succeeded
Many CLIs call isatty(1) before choosing a mode.
No TTY means no color, no pager, extra prompts.
python3 -c 'import sys; print("tty", sys.stdout.isatty())'
echo "TERM=${TERM:-unset}"
echo "CI=${CI:-unset}"
git config --get core.pager || true
git log may block inside less without a TTY.
npm may emit extra noise when CI stays unset.
The agent then "presses enter" inside a fantasy TTY.
There is no enter key on that headless box.
There is SIGPIPE when the pager never starts.
There is a stuck job when it does start.
Corrected model: non-interactive is a different program.
Same binary, different flags, and very different exits.
Force the non-interactive path on purpose every run.
export CI=true
export GIT_PAGER=cat
export PAGER=cat
export TERM=dumb
git --no-pager log -1 --oneline
Myth 5: Redirecting to a file matches your terminal
Agents often run task.sh > log.txt 2>&1.
That merge looks complete and still lacks timestamps.
Stdout and stderr can lose order under load.
A crash on stderr can land above a success print.
cat > /tmp/order.py <<'PY'
import sys, time
print("ok", flush=True)
time.sleep(0.1)
print("boom", file=sys.stderr, flush=True)
PY
python3 /tmp/order.py > /tmp/merged.log 2>&1
cat /tmp/merged.log
file /tmp/merged.log
od -c /tmp/merged.log | head
Did a CRLF sneak in from a previous checkout?
Did ANSI cursor moves overwrite a failure line?
Corrected model: a log file is only an artifact.
It is not a terminal session you actually witnessed.
The artifact: a sixty-second stdio canary
Do not argue with the model about the hang.
Ask the box, save JSON, and compare both machines.
Label: this is a proposed script, not a published benchmark.
I am not reporting timings, pass rates, or hardware here.
#!/usr/bin/env bash
# stdio_canary.sh — proposed checks, not a scored suite
set -euo pipefail
out="${1:-./stdio-canary.json}"
py="$(command -v python3 || command -v python)"
tty_out="$("$py" -c 'import sys; print("true" if sys.stdout.isatty() else "false")')"
tty_err="$("$py" -c 'import sys; print("true" if sys.stderr.isatty() else "false")')"
set +e
set -o pipefail
cat /this/path/does/not/exist 2>/dev/null | wc -l >/dev/null
pipe_rc=$?
set -euo pipefail
printf '%s\n' '{' \
" \"tty_stdout\": ${tty_out}," \
" \"tty_stderr\": ${tty_err}," \
" \"term\": \"${TERM:-}\"," \
" \"ci\": \"${CI:-}\"," \
" \"shell\": \"${SHELL:-}\"," \
" \"pipefail_missing_file_exit\": ${pipe_rc}," \
" \"python\": \"${py}\"" \
'}' > "$out"
echo "wrote $out"
cat "$out"
Run it locally, then run it on the remote box.
Diff the two JSON files; that diff is the lesson.
chmod +x stdio_canary.sh
./stdio_canary.sh /tmp/local.json
# same command on the remote shell
diff -u /tmp/local.json /tmp/remote.json || true
Decision checklist
Use this list when the agent report and the box disagree.
- Check
isattyon both machines before you debug. A false remote TTY means you need-uand--yes. - Assume
pipefailis off until a wrapper sets it. Putset -o pipefailat the wrapper top. - Assume git will still launch less on headless boxes. Export
GIT_PAGER=catbefore any git log command. - Assume Python will block-buffer on the remote pipe. Export
PYTHONUNBUFFERED=1for every remote agent run. - Never treat the last log line as the verdict. Capture status in the same script and print it.
A wrapper I want every agent to call
Label: this is proposed wrapper code, not a vendor feature.
#!/usr/bin/env bash
set -euo pipefail
export PYTHONUNBUFFERED=1
export GIT_PAGER=cat
export PAGER=cat
export CI=true
export TERM="${TERM:-dumb}"
log="$(mktemp)"
rc=0
"$@" >"$log" 2>&1 || rc=$?
cat "$log"
echo "CANARY_EXIT=$rc"
exit "$rc"
Call it with the real test command behind the wrapper.
chmod +x agent_wrap.sh
./agent_wrap.sh pytest -q
Now the exit code is a line in the log.
The model can ignore it; you should not.
Limitations
This canary does not measure model quality at all.
It does not prove the free server has no other jobs.
It will not catch a child killed by SIGKILL.
It will not decode ANSI home-cursor rewrite tricks.
The JSON keys are a snapshot, not a security audit.
Do not paste secrets into the canary output file.
Windows cmd.exe has different buffering rules entirely.
This FAQ is POSIX-shaped on purpose. Know that limit.
Who should not use this approach
Skip it if you only drive GUI tools today.
Skip it if your agent never opens a shell.
Do not use a free shared box for production secrets.
Do not treat the canary as a substitute for real tests.
If you truly need a TTY, allocate a real TTY.
ssh -t exists. Wishful "press enter" prompts do not.
What I do after the diff
I keep the remote JSON next to the pull request.
If tty_stdout flipped, I change the flags first.
I do not ask the model why the hang happened.
I ask whether stdout was a pipe that night.
Need a second opinion from a free remote shell?
Drop the canary on MonkeyCode's free server and diff it.
Then read CANARY_EXIT and ignore the chat flourish.
Top comments (0)