$ backgroundclaude
blog · 2026-04-11 · 6 min read

Turn claude -p into a multi-turn REPL with one named pipe

The documented story is that claude -p is one-shot: you send a prompt, it runs, it exits. If you want multiple turns you reach for --continue or --resume, each of which starts a fresh process and pulls the previous session off disk. That works, but it's one process per turn and it doesn't help you drive a live session from a shell.

The undocumented story is that --input-format stream-json plus a named pipe plus one file-descriptor trick gives you a single, persistent claude -pprocess you can type at from any other shell command. It's eight lines of bash. Here's the whole recipe, followed by the line-by-line explanation.

The recipe

mkfifo input_pipe

cat input_pipe | claude -p \
  --input-format stream-json \
  --output-format stream-json \
  --verbose \
  | jq --unbuffered -r '
      if .type == "assistant" then
        (.message.content[] | select(.type == "text") | .text)
      elif .type == "result" then
        "--- done (cost: \(.total_cost_usd)) ---"
      else
        empty
      end' &

exec 3>input_pipe

# Send messages:
echo '{"type":"user","message":{"role":"user","content":"Hello"}}' >&3

# Cleanup when done:
exec 3>&-
rm input_pipe

Paste that into a bash shell. After the first echo, Claude's reply streams into your terminal. You can echo more messages any time you like; the session stays alive. When you close fd 3, the whole pipeline tears down cleanly.

Why each piece matters

mkfifo input_pipe

Creates a named pipe (FIFO) on the filesystem. A FIFO looks like a regular file in ls -l but acts like an in-memory pipe: writers queue data, readers drain it. The big difference from an anonymous shell pipe (|) is that a FIFO has a name, so anyone on the machine can open it for writing without being part of the same pipeline.

cat input_pipe | claude -p … &

A three-stage background pipeline:

exec 3>input_pipe — the trick

This is the part most tutorials miss. FIFOs close as soon as the last writer goes away. If you ran echo ... > input_pipe directly, each echo would open the FIFO, write, and close — which would send EOF to cat, which would send EOF to claude, which would exit. One message per session.

exec 3>input_pipe opens file descriptor 3 as a persistent writer to the FIFO. As long as fd 3 is open, the FIFO stays open, which keeps cat reading, which keeps claude alive. Every echo ... >&3 after that just streams more data into the existing pipe without closing it.

When you're done, exec 3>&- closes fd 3, which closes the last writer, which cascades EOF down the pipeline and gives you a clean shutdown.

echo '{..."user"...}' >&3

Send a user message. The JSON event shape is what --input-format stream-json expects: a type of user and a messagewith role and content. Every event has to fit on a single line (it's newline-delimited), so if you're sending multi-line prompts you want to JSON-encode them with jq -c or python -c:

PROMPT="Explain this error message:
$(cat /tmp/err.log)"

jq -nc --arg c "$PROMPT" \
  '{type:"user", message:{role:"user", content:$c}}' >&3

The jq filter, expanded

That one-liner is doing real work. Here's what each branch means:

What this composes with

Because input is “anything that writes to fd 3” and output is “anything that reads stdout of the pipeline,” this pattern composes with every UNIX primitive you already know:

Gotchas

The other half of the pipeline

This post is about the input side of stream-json. If you want to understand the output side — system/api_retryevents, partial token deltas, and the ~30-line Node consumer that handles line-buffering correctly — that's the stream-json output deep dive. Together, input-stream-json and output-stream-json turn claude -p into a full-duplex agent protocol over stdio, which is what every background agent loop is actually built on.

Where the FIFO pattern stops

It's one process, one shell, one human (or script) at a time. The moment you want:

…you've outgrown the FIFO and you're describing a background agent loop. Same claude -p underneath, but now with worktree isolation, webhook handlers, and streamed events posted back to the issue tracker that triggered the run. See also git worktree for Claude Code — the isolation primitive that makes parallelism safe.

Takeaways

one FIFO isn't a fleet

Same protocol. Fleet scale. Cyrus.

Cyrus runs claude -p over stream-json per Linear issue, in isolated git worktrees, with rich mid-run approvals and streamed events back to the issue. Community self-hosted is free forever, BYOK across Claude, Codex, Cursor, and Gemini.

Try Cyrus free →