From tmux-codex-chat
Send one prompt to the OpenAI Codex CLI running in another tmux pane, submit it, wait until Codex's answer is captured, and report it back. Use when the user wants to consult Codex, ask Codex, get Codex's opinion, have Codex review something, or route a question to a Codex CLI in a separate tmux pane — phrases like "codex に聞いて", "ask codex", "consult codex", "別 pane の codex にレビューさせて", "%138 の codex に <X>", "codex の意見が欲しい". Pane detection looks for `pane_current_command` in {codex, node} and confirms via Codex UI signals (`›` input prompt, status line with model name and "Context X% used", `─ Worked for ... ─` separator, or the `OpenAI Codex` startup banner). Only panes inside the current tmux session are considered — other sessions are never targeted, even if explicitly named. If multiple Codex panes are found or none can be confirmed, ask the user which pane id to target before sending. Prefer this over the general-purpose tmux-pane-send / tmux-pane-exec skills whenever the target is specifically a Codex CLI and the user expects a captured answer back.
How this skill is triggered — by the user, by Claude, or both
Slash command
/tmux-codex-chat:tmux-codex-chatThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
One invocation = one prompt → one captured answer. Follow-ups require re-invocation. The skill never presses `y`/`n`/`Enter`/`Esc` on a Codex approval dialog — if one appears, the run is interrupted and surfaced.
One invocation = one prompt → one captured answer. Follow-ups require re-invocation. The skill never presses y/n/Enter/Esc on a Codex approval dialog — if one appears, the run is interrupted and surfaced.
The skill injects a unique marker [CODEX_CHAT_REQ:<full-uuid>] into the visible prompt and creates a pending file. Codex's Stop hook (~/.codex/hooks/tmux-codex-chat-stop.sh) parses the latest user message of the transcript via jq -rs, and if the extracted UUID has a matching pending file it atomically writes $RUNDIR/done-<uuid>.json. The skill blocks on that file.
Approval dialogs do not end Codex's turn, so Stop never fires while paused on one — a parallel watcher subshell scans the pane every ~2 s (with a 0.5 s burst for the first 10 s) for dialog patterns and trips an approval sentinel file instead.
$RUNDIR = /tmp/codex-chat-$UID (mode 700, ownership-checked) holds: pending-<uuid>, done-<uuid>.json, approval-<uuid>.txt, prompt-<ts>-<uuid>.md, and stop-hook.log.
~/.codex/config.toml has features.codex_hooks = true.~/.codex/hooks.json registers a Stop hook pointing at ~/.codex/hooks/tmux-codex-chat-stop.sh.jq is on PATH.hooks.json once at session boot and does not reload it. If hook config changed since the target pane was started, the user must Ctrl-D//exit and restart Codex in that pane — otherwise this skill silently times out.Health-check (use exactly; the precedence in older one-liners was wrong):
hook_ok() {
test -x ~/.codex/hooks/tmux-codex-chat-stop.sh || return 1
jq -e '.hooks.Stop' ~/.codex/hooks.json >/dev/null 2>&1 || return 1
grep -qE 'codex_hooks[[:space:]]*=[[:space:]]*true' ~/.codex/config.toml || return 1
command -v jq >/dev/null
}
hook_ok && echo OK_HOOK_READY || echo MISSING
If MISSING, fall back to UI polling (final section) and tell the user once.
[ -n "$TMUX" ] || { echo "Not inside tmux"; exit 1; }
SESSION=$(tmux display-message -p '#S')
All pane operations target $SESSION. Other tmux sessions are out of scope.
tmux list-panes -s -t "$SESSION" -F "#{pane_id} #{pane_current_command} #{pane_current_path}"
A pane is a candidate when pane_current_command is codex (high) or node (medium — Codex runs as Node and often shows up that way). Capture each candidate (tmux capture-pane -t %N -p -S -) and require ≥1 of:
^[[:space:]]*› (U+203A input prompt) — confirmedContext [0-9]+% used — confirmed─ Worked for .* ─ — confirmedOpenAI Codex near model: and directory: — confirmed (rescues fresh panes)gpt-5 / gpt-5\.[0-9] near status — supporting onlyDecision: 1 confirmed → use it; ≥2 → list as session:window.pane and ask; 0 → ask. Never guess.
tmux display-message -p -t %N "#{pane_id} #{session_name} #{pane_current_command}"
If the pane is in a different session, refuse — do not silently retarget. Then check busy state: any of the following means not-ready, surface the capture and ask the user (wait / cancel / interrupt) — the skill never presses Esc/Ctrl-C itself.
› line is non-empty (residual input)Working (XXs • esc to interrupt) visible› prompt at all (mid-stream)The Create a plan? shift + tab use Plan mode esc dismiss hint is not a busy signal.
RUNDIR="/tmp/codex-chat-${UID:-$(id -u)}"
mkdir -m 700 -p "$RUNDIR" && chmod 700 "$RUNDIR"
[ -O "$RUNDIR" ] || { echo "$RUNDIR not owned by us"; exit 1; }
REQ=$(/usr/bin/uuidgen) # absolute path bypasses any user alias
PENDING="$RUNDIR/pending-$REQ"
DONE_FILE="$RUNDIR/done-$REQ.json"
APPROVAL_FILE="$RUNDIR/approval-$REQ.txt"
PANE="%14" # the confirmed Codex pane id from §1
touch "$PENDING" # pending-file gate; hook only writes done if this exists
The pending file is the load-bearing safety mechanism — it must be created BEFORE submission and it ensures stale markers in older transcript turns cannot ever overwrite a current run.
The marker must appear in the visible user message, not only inside a referenced file — if Codex never reads the file, the hook will see no marker and the run will time out (better than writing to the wrong done-file). Two send paths:
tmux send-keys -t "$PANE" -l "[CODEX_CHAT_REQ:$REQ] (internal routing tag — please ignore in your reply) <flattened safe text>"
tmux send-keys -t "$PANE" Enter
ts=$(date +%Y%m%d-%H%M%S)
PROMPT_FILE="$RUNDIR/prompt-$ts-$REQ.md"
umask 077
cat > "$PROMPT_FILE" <<'EOF'
[CODEX_CHAT_REQ:__REQ__]
(internal routing tag — please ignore in your reply)
<full prompt body — any characters, the heredoc terminator is single-quoted>
EOF
sed -i '' "s/__REQ__/$REQ/" "$PROMPT_FILE"
ref="[CODEX_CHAT_REQ:$REQ] Please read $PROMPT_FILE and respond to the request inside it. Do not echo or strip the [CODEX_CHAT_REQ:...] marker — it is internal."
printf '%s' "$ref" | tmux load-buffer -
tmux paste-buffer -t "$PANE"
tmux send-keys -t "$PANE" Enter
Notes:
tmux load-buffer - overwrites tmux clipboard buffer 0; mention to the user if they care about it.rm "$RUNDIR/prompt-*.md".send-keys -l call. Each \n is its own keystroke and Codex mis-submits.Enter is its own send-keys call without -l. (-l Enter types the letters.)Wait ~1 s, capture once. If the prompt body still sits visibly above the › line, send Enter once more as a standalone key. If it still hasn't submitted, stop, surface the capture, report. Do not try a third Enter / C-j / C-m.
# Parallel approval watcher: 0.5s for first 10s, then 2s. Exits when either
# sentinel appears, so we don't strictly need to kill it.
(
i=0
while :; do
[ -f "$DONE_FILE" ] && exit 0
cap=$(tmux capture-pane -t "$PANE" -p 2>/dev/null) || exit 0
if printf '%s' "$cap" | grep -qE \
'(\(y/n\)|\[[yY]/[nN]\]|Apply this patch\?|Allow command:|Run command\?|Allow this command\?|Always allow|Approve and run|Proceed\?|Continue\?|\(esc to cancel\)|\(esc to dismiss\)|^[[:space:]]*[❯>][[:space:]]+(Yes|No|Allow|Deny|Approve|Run|Apply|Proceed|Continue))'; then
printf '%s' "$cap" > "$APPROVAL_FILE"
exit 0
fi
i=$((i+1))
if [ "$i" -lt 20 ]; then sleep 0.5; else sleep 2; fi
done
) &
WATCHER_PID=$!
# Block on whichever sentinel arrives. Default deadline is 5 min; bump
# to 600/900 if the prompt is a long review or repo-wide audit (see the
# `timeout)` branch below — once we hit the deadline, the answer is
# effectively unrecoverable through this skill).
DEADLINE=$(( $(date +%s) + 300 ))
RESULT=timeout
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
if [ -f "$DONE_FILE" ]; then RESULT=done; break; fi
if [ -f "$APPROVAL_FILE" ]; then RESULT=approval; break; fi
sleep 0.3
done
kill "$WATCHER_PID" 2>/dev/null
wait "$WATCHER_PID" 2>/dev/null || true
case "$RESULT" in
done)
ANSWER=$(jq -r '.last_assistant_message // ""' "$DONE_FILE")
rm -f "$DONE_FILE" "$PENDING" # pending should already be gone, idempotent
;;
approval)
DIALOG=$(cat "$APPROVAL_FILE")
rm -f "$APPROVAL_FILE"
# Pending file is left in place. Once the user resolves the dialog
# in the Codex pane, Codex will finish the turn and the Stop hook
# will write $DONE_FILE. The skill MUST surface the exact REQ and
# DONE_FILE path so the user can recover the answer without losing
# the routing context — either by re-invoking this skill with the
# same $REQ to wait again, or by reading $DONE_FILE manually.
cat <<EOF
APPROVAL DIALOG — Codex is paused waiting for your decision.
REQ: $REQ
DONE_FILE: $DONE_FILE
Resolve the dialog in the Codex pane (do not press keys from here).
Once Codex finishes, the answer will appear at \$DONE_FILE.
Dialog text follows:
$DIALOG
EOF
;;
timeout)
LATEST=$(tmux capture-pane -t "$PANE" -p)
rm -f "$PENDING" # prevent stale-replay risk on later turns
# IMPORTANT: removing the pending file means a *late* Codex completion
# (after the 5-minute window) will NOT produce a done-file — the hook
# silently skips because no pending exists for that REQ. This is
# intentional (otherwise stale markers in transcript could overwrite
# an unrelated future invocation), but it does mean the answer is
# effectively unrecoverable through this skill once we time out.
# Long-running reviews must either:
# (a) extend DEADLINE before invocation (e.g. 600 or 900 seconds),
# (b) be harvested manually from the pane scrollback, or
# (c) be re-asked in a fresh invocation.
cat <<EOF
TIMEOUT — Codex did not finish within the 5-minute window.
The pending file has been removed to prevent stale replay, so the
Stop hook will NOT write $DONE_FILE even if Codex finishes later.
Recover the answer manually from the pane scrollback, or re-invoke
the skill with a longer DEADLINE. Latest pane content follows:
$LATEST
EOF
;;
esac
Approval dialog patterns (used by the watcher above):
classic (y/n), [y/N], Apply this patch?, Allow command:; Codex-style Run command?, Allow this command?, Always allow, Approve and run, Proceed?, Continue?, (esc to cancel/dismiss); selector lines starting with ❯ or > followed by Yes/No/Allow/Deny/Approve/Run/Apply/Proceed/Continue.
Return: pane id, REQ, the prompt (summarized if long; include $PROMPT_FILE if file path was used), and last_assistant_message from the done file (preferred over re-capturing the pane — no terminal wrapping artifacts). Flag uncertainty: timed out, approval interrupted, fallback path used.
Do not claim Codex "approved", "completed", or "agreed" unless its captured text literally supports it.
The done/approval files of the current run are removed in §6. $PROMPT_FILE is intentionally retained. Periodically rm "$RUNDIR/prompt-*.md" "$RUNDIR/stop-hook.log".
If the health-check failed, fall through to a UI-string poll: baseline $(tmux capture-pane -t "$PANE" -p -S - | grep -c "Worked for") before sending, then poll every ~3 s and complete when the bottom shows an empty › AND no Working/spinner/approval AND either Worked for count exceeds baseline OR two consecutive captures are byte-identical. Apply the same approval short-circuit. 5-min soft timeout.
This path is best-effort only. It can mis-detect when output wraps mid-line, when prior › text is stale, when Codex changes its UI strings, or when the answer arrives faster than 3 s. Do not treat it as equivalent to the hook path. Tell the user the prerequisites failed (MISSING) so they fix it once and never come back here.
| Need | Command |
|---|---|
| Session name | tmux display-message -p '#S' |
| Panes (current session) | tmux list-panes -s -t "$SESSION" -F "#{pane_id} #{pane_current_command}" |
| Capture screen / scrollback | tmux capture-pane -t %N -p / tmux capture-pane -t %N -p -S - |
| Generate REQ | /usr/bin/uuidgen (absolute path — alias-proof) |
| Submit | tmux send-keys -t %N Enter (separate from -l text) |
| Wait for done | until [ -f "$DONE_FILE" ]; do sleep 0.3; done |
| Inspect last hook activity | tail "$RUNDIR/stop-hook.log" |
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub itouuuuuuuuu/claude-plugins --plugin tmux-codex-chat