DEV Community

Jordan Huang
Jordan Huang

Posted on

FAQ: Five Myths About a TTY the Agent Does Not Have

Ever watch an agent freeze on a yes-no prompt? The model keeps talking like nothing blocked it. The process on the box waits for stdin.

I keep seeing the same five claims in chat logs. People treat a headless agent box like a laptop. Then they blame the model when apt sits there.

This FAQ is about TTY myths. It is not about model quality. It is not about git identity either.

Why these myths spread

A chat UI looks interactive, right? You type. It types back. That feeling lies.

The free model writes a shell command. A free server may run that command. Those are two different processes with no shared keyboard.

I draft probes with MonkeyCode when I want a free model and a free server in the same loop. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The checklist still works if you paste it elsewhere.

The mental model that actually holds

Treat the pair like CI. CI has no human at stdin. CI has no pager waiting for q.

If a command needs a human, rewrite it. Do not narrate the prompt in chat. Kill the process and replace the flags.

Ask yourself one question before every install. Can this command finish with stdin closed?

Myth 1: The agent can answer "Are you sure?"

The claim

People say the agent will type Y. Why would it? The installer is not reading the chat transcript.

What you actually see

apt-get install foo stops on a confirmation line. The shell is blocked on read. The model may still invent a success sentence.

Evidence you can run

# Label: run this yourself. Do not trust chat narration.
sudo apt-get install foo
# Now in another pane, if you even have one:
ps -o pid,stat,wchan,cmd -C apt-get
Enter fullscreen mode Exit fullscreen mode

Look at wchan. A waiting read is not a thinking model. It is a stuck prompt.

Corrected model

Confirmation is a process, not a conversation. Reach for a noninteractive flag first.

sudo DEBIAN_FRONTEND=noninteractive apt-get install -y foo
Enter fullscreen mode Exit fullscreen mode

If you cannot find that flag, do not run it. Ask for a noninteractive equivalent before the box starts waiting.

Myth 2: yes | unlocks every prompt

The claim

Someone always pastes yes | in front of the command. It looks clever in a laptop demo. It fails on a headless box more often than it helps.

What you actually see

yes writes y into stdin. Many tools ignore stdin completely. They open /dev/tty instead.

Password prompts do this. SSH host-key checks do this. Some npm lifecycle scripts do this too.

Evidence you can run

# Looks clever. Often feeds a pipe nobody reads.
yes | ssh-keygen -t ed25519 -f /tmp/demo-key

# Same tool, flags that skip the prompt entirely.
ssh-keygen -t ed25519 -f /tmp/demo-key -N "" -q
Enter fullscreen mode Exit fullscreen mode

Did the first form hang on overwrite? That hang is /dev/tty, not stdin.

Corrected model

stdin is not a keyboard. /dev/tty is a different door. Prefer flags that disable the question.

Never pipe yes into a secret prompt. You will spray a pattern into logs. You may also lock an account.

Myth 3: A hung command means the model is thinking

The claim

The chat still streams tokens, so you wait. Bad instinct. Those two clocks are not coupled at all.

What you actually see

The box is blocked on read(). The model is guessing the next paragraph. You now have two liars in one window.

Evidence you can run

PID=$(pgrep -n -f 'apt-get|npm|pip|less' || true)
echo "pid=${PID:-none}"
[ -n "${PID:-}" ] && ps -o pid,stat,wchan,cmd -p "$PID"
# Optional. Stop after a few lines.
[ -n "${PID:-}" ] && timeout 5 strace -p "$PID" 2>&1 | head
Enter fullscreen mode Exit fullscreen mode

See read on fd 0? That is a prompt. See wait in wchan? That is a child, not a deep thought.

Corrected model

Token stream is not process state. Check the process. Ignore the narration until ps agrees.

Wrap unknown commands in timeout. A killed process is cheaper than a ten-minute shrug.

Myth 4: CI=true makes every installer quiet

The claim

One environment variable will silence apt, npm, pip, and git. Which universe is that from?

What you actually see

Some tools honor CI. Many do not. npm often does. apt does not care. pip depends on the package. cargo is its own world.

Evidence you can run

# npm: sometimes enough, never a contract
CI=true npm install

# more honest for npm in a clean tree
npm ci --yes --no-fund --no-audit

# pip: fail if it must ask
pip install --no-input some-pkg

# git: never open less
GIT_PAGER=cat git --no-pager log -1 --oneline
Enter fullscreen mode Exit fullscreen mode

Did npm install still stop on a funding prompt? Then CI=true was not the mute switch you needed.

Corrected model

Each toolchain has its own mute switch. Collect them in a wrapper. Do not trust one env var across ecosystems.

Myth 5: If a pager appeared, someone scrolled it

The claim

The chat showed the first screen of git log. So somebody hit space, right? Nobody did.

What you actually see

git log, systemctl status, man, and less assume a human. On a TTY they wait for q. Without a TTY they may still block.

Your chat captured the first page. The process is still inside the pager.

Evidence you can run

git --no-pager log -5 --oneline
export PAGER=cat
export GIT_PAGER=cat
export SYSTEMD_PAGER=cat
export MANPAGER=cat
systemctl --no-pager --full status || true
Enter fullscreen mode Exit fullscreen mode

If less is in ps, you did not finish the command. You screenshotted a waiting TUI.

Corrected model

Pagers are interactive programs. Ban them in agent shells. Set PAGER=cat before the first git command.

Artifact: a TTY probe and a fail-fast wrapper

Run this on the box before you install anything. Do not skip the printout. Label: unexecuted template. You run it.

#!/usr/bin/env bash
# probe-tty.sh
set -euo pipefail

python3 - <<'PY'
import os, sys
for fd, name in ((0, "stdin"), (1, "stdout"), (2, "stderr")):
    print(f"isatty({name})={os.isatty(fd)}")
PY

echo "TERM=${TERM-<unset>}"
echo "CI=${CI-<unset>}"
echo "DEBIAN_FRONTEND=${DEBIAN_FRONTEND-<unset>}"
echo "PAGER=${PAGER-<unset>}"
echo "GIT_PAGER=${GIT_PAGER-<unset>}"

if [ -t 0 ]; then
  echo "stdin is a TTY. Interactive tools may wait for you."
else
  echo "stdin is not a TTY. Prompts will hang or fail."
fi
Enter fullscreen mode Exit fullscreen mode

Then wrap risky commands so a hidden prompt dies quickly.

#!/usr/bin/env bash
# run-noninteractive.sh
# 45s is a local guess, not a product SLA.
set -euo pipefail
cmd=("$@")
timeout --signal=TERM --kill-after=5s 45s env \
  DEBIAN_FRONTEND=noninteractive \
  CI=true \
  PAGER=cat \
  GIT_PAGER=cat \
  SYSTEMD_PAGER=cat \
  "${cmd[@]}"
Enter fullscreen mode Exit fullscreen mode

Use it like this.

chmod +x probe-tty.sh run-noninteractive.sh
./probe-tty.sh
./run-noninteractive.sh npm ci --yes --no-fund --no-audit
Enter fullscreen mode Exit fullscreen mode

If timeout fires, you found a hidden prompt. Do not retry the same command. Change flags and probe again.

A ten-minute drill

  1. Print isatty for stdin, stdout, and stderr.
  2. Export pager variables before any git or systemctl call.
  3. Rewrite one install command with explicit yes-flags.
  4. Run that rewrite under timeout.
  5. If it dies, read ps and wchan, not the chat.

That drill is the whole workflow. The model drafts. The box executes. You believe ps.

Decision table

You wanted Interactive default Noninteractive stand-in If no stand-in exists
apt install confirm + pager DEBIAN_FRONTEND=noninteractive apt-get -y skip the package
npm init many questions npm init -y write package.json yourself
npm install funding / audit prompts npm ci --yes --no-fund --no-audit vendor the tree
pip install maybe confirm pip install --no-input vendor a wheel
git log less git --no-pager plus GIT_PAGER=cat print git log -1 only
ssh-keygen overwrite prompt -N "" -f path -q do not generate keys here
docker pull rare auth prompt check credentials first do not pull private images

Print the table next to the probe output. Pick the stand-in before the model invents yes |.

Limitations

This does not make interactive tools safe. It only fails faster.

timeout can kill a slow but honest compile. Forty-five seconds is a guess. Raise it for real builds. Keep it short for package managers that should not compile the world.

The probe cannot see /dev/tty opens after fork. A child can still block. strace helps. It is not complete coverage.

Do not put credentials in flags the model can see. Do not use this wrapper as a production installer. This is a debugging workflow for hung chats.

Who should skip this

Skip this if you have a real terminal attached. Skip this if you debug inside a multiplexer. Skip this if the job is a TUI.

Skip this for anything that needs a password. A password prompt is not a yes prompt. Do not flatten those two cases.

Skip this if you need a long-running dev server with a human attached. That is a laptop job. A free server is the wrong shape for that work.

What I want back

Run the probe. Paste the three isatty lines. Which myth bit you first?

Top comments (0)