Skip to content

37 critical repl triggering

Part VII: Ecosystem, Runtime & Agent-Specific | Challenge §37

37. REPL / Interactive Mode Accidental Triggering

Severity: Critical | Frequency: Situational | Detectability: Hard | Token Spend: High | Time: Critical | Context: Low

The Problem

Some CLI tools expose a REPL (Read-Eval-Print Loop) or interactive shell mode — either as an explicit subcommand (my-tool shell, my-tool repl) or as a flag triggered under certain conditions. Python Fire's --interactive flag is the canonical example: passing it drops the user into an IPython shell with the object in scope. If an agent constructs an invocation that includes --interactive (e.g., from a misread help text, a hallucinated flag, or a misconfigured test), the process hangs indefinitely.

This is distinct from challenge #10 (Interactivity & TTY Requirements), which concerns prompt() and confirm() calls — single-question interactive pauses. A REPL is an ongoing interactive loop that cannot be resolved by sending a single stdin input. Even sending quit\n or exit\n to stdin only works if the REPL is actually reading from stdin, which is not guaranteed.

# Fire CLI: agent passes --interactive by accident
python my_fire_cli.py process_data --dataset=path/to/data.csv --interactive
# Drops into IPython: "Python 3.x.x | IPython x.x.x"
# Process is now waiting for interactive input forever
# No stderr output, no error, exit code never comes

Beyond Python Fire, this pattern appears in: - Any tool with a shell or repl subcommand that does not check for TTY before launching - python -c calls inside tools that may be executed with -i flag - Database CLI tools (psql, mysql) invoked without a query — they drop into interactive mode - YAML/JSON editors that open an interactive editor when no explicit value is provided

Impact

  • Process hangs until the agent's external timeout fires (challenge #11), consuming the full timeout budget
  • The agent may interpret the hung process as a slow operation and retry, creating multiple hanging processes (challenge #17)
  • Unlike prompt() which produces visible output ("Enter value:"), a REPL may produce a welcome banner that the agent misinterprets as success output
  • On some terminals, the REPL changes terminal state (raw mode), potentially corrupting subsequent output
  • No structured error is produced — the hang is silent from the framework's perspective

Solutions

For CLI authors:

import sys

# Gate ALL REPL/interactive modes behind TTY check
@app.command()
def shell():
    if not sys.stdin.isatty():
        print(json.dumps({"ok": False, "error": {"code": "INTERACTIVE_REQUIRED",
            "message": "Shell mode requires an interactive terminal. Run without redirection."}}))
        sys.exit(2)
    launch_repl()

# Python Fire: never register --interactive as a reachable flag in non-TTY environments

For agents:

# Scan --help output for REPL-triggering flags before first invocation
REPL_FLAGS = {"--interactive", "--shell", "--repl", "-i"}
help_output = subprocess.run([tool, "--help"], capture_output=True).stdout.decode()
risky_flags = [f for f in REPL_FLAGS if f in help_output]
# Avoid those flags; set stdin=subprocess.DEVNULL to prevent stdin reads

result = subprocess.run(cmd, stdin=subprocess.DEVNULL, capture_output=True, timeout=30)

For framework design: - Any command flagged as interactive=True or mode="repl" must gate on sys.stdin.isatty() and return a structured error if the check fails, rather than attempting to launch - Provide a framework-level REPL_GUARD decorator that wraps REPL-launching commands - Default stdin=subprocess.DEVNULL in all framework-generated subprocess calls and test harnesses - Document in --schema output which commands require interactive mode, so agents can skip them

Evaluation

Score Condition
0 REPL/interactive mode launches in non-TTY without check; hangs indefinitely; no error emitted
1 TTY check exists but fails silently or hangs rather than emitting a structured error
2 Non-TTY invocation of REPL mode exits immediately with a structured INTERACTIVE_REQUIRED error (exit 2)
3 --schema flags commands as "requires_interactive": true; framework REPL_GUARD prevents non-TTY launch at registration time

Check: Invoke any REPL/shell subcommand with stdin=subprocess.DEVNULL — verify it exits within 1 second with a structured JSON error, not a hang.


Agent Workaround

Always set stdin=DEVNULL and scan for REPL-triggering flags before first invocation:

import subprocess, re

REPL_FLAGS = {"--interactive", "--shell", "--repl", "-i", "--console"}

def has_repl_risk(tool: str) -> set[str]:
    """Check help text for REPL-triggering flags."""
    result = subprocess.run(
        [tool, "--help"],
        capture_output=True, text=True,
        stdin=subprocess.DEVNULL,
        timeout=10,
    )
    found = set()
    for flag in REPL_FLAGS:
        if flag in result.stdout or flag in result.stderr:
            found.add(flag)
    return found

risky = has_repl_risk("tool")
if risky:
    print(f"WARNING: Tool exposes REPL flags {risky} — never pass these to tool calls")

# All subprocess calls: stdin=DEVNULL prevents any blocking stdin read
result = subprocess.run(
    ["tool", "deploy", "--output", "json"],
    capture_output=True, text=True,
    stdin=subprocess.DEVNULL,  # critical: prevents any blocking read
    timeout=60,
)

Kill a hung REPL invocation and mark it as an interactive-required failure:

import subprocess, signal

try:
    result = subprocess.run(
        cmd,
        capture_output=True, text=True,
        stdin=subprocess.DEVNULL,
        timeout=10,
    )
except subprocess.TimeoutExpired as e:
    e.process.send_signal(signal.SIGTERM)
    raise RuntimeError(
        "Command timed out — may have launched a REPL or interactive mode. "
        "Check for --shell/--repl/--interactive flags and avoid them."
    )

Limitation: If a tool launches a REPL unconditionally with no TTY check and ignores DEVNULL (e.g., reads from /dev/tty directly), the only defense is to kill the process after a short timeout and treat it as an interactive-required failure