DEV Community

Jordan Huang
Jordan Huang

Posted on

FAQ: Five Stdio Myths a Headless Agent Box Will Hide

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=$?"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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=$?"
Enter fullscreen mode Exit fullscreen mode

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=$?"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Decision checklist

Use this list when the agent report and the box disagree.

  1. Check isatty on both machines before you debug. A false remote TTY means you need -u and --yes.
  2. Assume pipefail is off until a wrapper sets it. Put set -o pipefail at the wrapper top.
  3. Assume git will still launch less on headless boxes. Export GIT_PAGER=cat before any git log command.
  4. Assume Python will block-buffer on the remote pipe. Export PYTHONUNBUFFERED=1 for every remote agent run.
  5. 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"
Enter fullscreen mode Exit fullscreen mode

Call it with the real test command behind the wrapper.

chmod +x agent_wrap.sh
./agent_wrap.sh pytest -q
Enter fullscreen mode Exit fullscreen mode

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)