API-to-tmux Pipeline—Injecting Prompts into Claude Code CLI via Container
You can do this by piping the text via an API → File → Container → Tmux Send Keys pipeline.
It's a pretty clean pipeline once you break it into layers. Here's how each piece works and how they connect.
The Full Pipeline
API response → file → container → tmux send-keys → Claude Code stdin
Approach 1: Non-interactive (claude -p)
The cleanest path if you don't need a persistent interactive session:
# 1. Fetch from API, write to shared volume file curl -s https://your-api/prompt \ -H "Authorization: Bearer $TOKEN" \ | jq -r '.text' > ./shared/prompt.txt # 2. Inside the container, pipe it into Claude Code in print mode docker exec my-container bash -c "cat /shared/prompt.txt | claude -p"
The -p / --print flag runs Claude Code non-interactively, reads stdin, prints output, and exits — no tmux needed.
Approach 2: Send to a live tmux session (interactive)
If you need to inject into a running Claude Code session:
# 1. Write API response to a file mounted into the container curl -s https://your-api/prompt | jq -r '.text' > ./shared/prompt.txt # 2. From inside the container (or exec into it), send to tmux pane docker exec my-container bash -c ' PROMPT=$(cat /shared/prompt.txt) tmux send-keys -t claude-session:0.0 "$PROMPT" Enter '
The tmux send-keys target format is session:window.pane — adjust to match your session name.
Full Script Example
#!/usr/bin/env bash
set -euo pipefail
CONTAINER="claude-runner"
TMUX_TARGET="claude-session:0.0"
SHARED_FILE="./shared/prompt.txt"
API_URL="https://your-api/endpoint"
# Step 1: Fetch and write
echo "Fetching from API..."
curl -s "$API_URL" \
-H "Authorization: Bearer ${API_TOKEN}" \
| jq -r '.prompt' > "$SHARED_FILE"
# Step 2: Send to tmux session in container
echo "Sending to Claude Code session..."
docker exec "$CONTAINER" bash -c \
"tmux send-keys -t '$TMUX_TARGET' \"\$(cat /shared/prompt.txt)\" Enter"
echo "Done."
Container Setup Requirements
Your container needs:
tmuxinstalledclaudeCLI installed and authenticated- The shared volume mounted:
-v $(pwd)/shared:/shared - A running tmux session:
tmux new-session -d -s claude-session claude
# Dockerfile snippet RUN npm install -g @anthropic-ai/claude-code RUN apt-get install -y tmux # Entrypoint starts tmux with claude CMD ["tmux", "new-session", "-s", "claude-session", "claude"]
Mounting Claude Code Subscription Credentials from the Host
If you pay for a Claude Code subscription (Pro/Max) instead of using a raw ANTHROPIC_API_KEY, the container needs access to the OAuth credentials and config that the claude CLI writes to your host. These all live under ~/.claude on the host (plus ~/.claude.json for the top-level config).
What to mount
| Host path | Container path | Purpose |
|---|---|---|
~/.claude |
/root/.claude |
OAuth tokens (.credentials.json), session state, projects, todos, shell snapshots |
~/.claude.json |
/root/.claude.json |
Top-level CLI config (account, login method, MCP servers, trusted dirs) |
Note: on macOS the credentials are stored in the Keychain, not in ~/.claude/.credentials.json. Before mounting, run claude once on a Linux box (or inside the container itself) to log in — that forces the file-based credential store. Alternatively, copy them out of Keychain via security find-generic-password -s "Claude Code-credentials" -w.
docker run
docker run -it --rm \ -v "$HOME/.claude:/root/.claude" \ -v "$HOME/.claude.json:/root/.claude.json" \ -v "$(pwd)/shared:/shared" \ --name claude-runner \ my-claude-image
docker-compose
services:
claude-runner:
image: my-claude-image
container_name: claude-runner
volumes:
- ${HOME}/.claude:/root/.claude
- ${HOME}/.claude.json:/root/.claude.json
- ./shared:/shared
tty: true
stdin_open: true
Permissions gotchas
- UID mismatch: if the container runs as a non-root user, the mounted files must be readable/writable by that UID. Either run the container as your host UID (
--user "$(id -u):$(id -g)") orchown -Rthe mount target inside the container's entrypoint. - Read-only is not enough: the CLI rotates OAuth refresh tokens, so the mount must be read-write —
:rowill break re-auth after the access token expires. - Don't bake credentials into the image: never
COPY ~/.claudein a Dockerfile. Always bind-mount at runtime so credentials never end up in image layers or registry caches. - Single-writer rule: don't run the host
claudeCLI and the containerized one against the same~/.claudesimultaneously — concurrent token refreshes can clobber.credentials.json. - macOS bind-mount perf: add
:cached(-v "$HOME/.claude:/root/.claude:cached") to avoid the FS sync tax on macOS Docker Desktop.
Verifying it worked
docker exec claude-runner claude --version docker exec claude-runner claude -p "say hi" # should respond without prompting for login
If the second command asks you to log in, the credentials aren't being seen — re-check the mount paths and the container's $HOME (it must match the directory where .claude/ was mounted).
Gotchas
| Issue | Fix |
|---|---|
Multiline prompts break send-keys |
Use a heredoc or escape newlines with \n, or use -p mode instead |
| Tmux session not found | Make sure the session name matches exactly; use tmux ls inside the container to verify |
| Auth not available in container | Mount ~/.claude or set ANTHROPIC_API_KEY as an env var |
| Race condition (API slow, tmux not ready) | Add a sleep or poll with tmux has-session before sending |
If you're doing this repeatedly/programmatically, the -p non-interactive mode is more reliable than send-keys since it doesn't depend on terminal state. send-keys shines when you want a human-watchable session that also accepts programmatic input.