Skip to content

Instantly share code, notes, and snippets.

@anaimi
Last active April 11, 2026 18:17
Show Gist options
  • Select an option

  • Save anaimi/21876bc315a7cfd99fea2a0627b9ef15 to your computer and use it in GitHub Desktop.

Select an option

Save anaimi/21876bc315a7cfd99fea2a0627b9ef15 to your computer and use it in GitHub Desktop.
Code Kibitzer
#!/usr/bin/env bash
#
# kibitzer.sh
# Silent second-opinion code reviewer for Claude Code, powered by Codex.
#
# ── Objective ────────────────────────────────────────────────────────────────
#
# A 'kibitzer' is someone who watches others (especially at chess
# or cards) and offers unsolicited advice or commentary from the sidelines.
#
# This script is a kibitzer for Claude Code (CC).
#
# It watches a Claude Code (CC) tmux pane, when idle, sends latest activity
# to Codex for review, and injects findings back into CC.
#
#
# ── How it works ─────────────────────────────────────────────────────────────
#
# You are expected to be using tmux, with a Claude Code (CC) pane named 'cc'.
#
# 1. Poll CC's tmux pane via capture-pane
# 2. Detect idle (output stable for IDLE_SECS)
# 3. Diff against previous capture to extract new activity
# 4. Send the last 50 lines to Codex via `codex exec resume --full-auto`
# 5. Classify Codex's one-line reply:
# SILENT → do nothing (default)
# NOTE: <msg> → soft-inject "# kibitzer: <msg>" into CC
# STOP: <msg> → Escape + hard-inject "# kibitzer STOP: <msg>"
# 6. STOP hard-interrupts CC (Escape + inject); NOTE soft-injects
#
# ── Usage ────────────────────────────────────────────────────────────────────
#
# ./kibitzer.sh seed Create initial Codex session
# ./kibitzer.sh run Main loop (background this)
# ./kibitzer.sh once Single tick, for debugging
# ./kibitzer.sh test Run unit tests (no tmux needed)
#
# ── Examples ─────────────────────────────────────────────────────────────────
#
# # First time — seed Codex with the kibitzer role:
# REPO_DIR=/var/www/myapp ./kibitzer.sh seed
#
# # Start watching (find your CC pane with: tmux list-panes -a):
# CC_PANE=mysession:1.0 REPO_DIR=/var/www/myapp ./kibitzer.sh run &
#
# # Stop:
# kill $(cat /tmp/kibitzer.pid)
#
# ── Customization ────────────────────────────────────────────────────────────
#
# CC_PANE tmux pane for Claude Code (default: cc)
# IDLE_SECS seconds of idle before reviewing (default: 5)
# POLL_SECS polling interval (default: 3)
# REPO_DIR working directory for codex exec (default: .)
# CODEX_SESSION session id to resume (default: --last)
# PIDFILE where to write the PID (default: /tmp/kibitzer.pid)
#
# ── Requirements ─────────────────────────────────────────────────────────────
#
# - tmux (running, with CC in a pane)
# - codex CLI (npm i -g @openai/codex)
# - REPO_DIR must be a git repo (codex exec requirement)
#
# ─────────────────────────────────────────────────────────────────────────────
set -uo pipefail
# ── Configuration ────────────────────────────────────────────────────────────
CC_PANE="${CC_PANE:-cc}"
IDLE_SECS="${IDLE_SECS:-5}"
POLL_SECS="${POLL_SECS:-3}"
REPO_DIR="${REPO_DIR:-.}"
CODEX_SESSION="${CODEX_SESSION:-}"
PIDFILE="${PIDFILE:-/tmp/kibitzer.pid}"
# ── Prompts ──────────────────────────────────────────────────────────────────
SEED_PROMPT='You are a silent code review kibitzer.
When asked to review Claude Code (CC) activity, respond with exactly one line:
SILENT, NOTE: <msg>, or STOP: <msg>. Default SILENT. Max 40 words.
Study the code base carefully in order to be useful and relevant. You comments
should be concise and to the point. Focus on:
- architecure (opportunity for simplification or better petterns or alternative approaches)
- logical bugs, race conditions
- security (writing into unowned record, reading unowned record, raw error msg leak, etc)
- code smell
etc
Pay attention to instructions in AGENTS.md and CLAUDE.md.
Do not make code edits.
Acknowledge.'
REVIEW_PROMPT='Review this CC activity for critical issues.
Respond with exactly one line: SILENT, NOTE: <msg>, or STOP: <msg>.
Default SILENT. Max 40 words.'
# ── IO (overridable for tests via CAPTURE_CMD, SEND_CMD, CODEX_CMD) ─────────
: "${CAPTURE_CMD:=tmux capture-pane -p -S -400 -t}"
: "${SEND_CMD:=tmux send-keys -t}"
capture() { $CAPTURE_CMD "$1"; }
send_escape() { $SEND_CMD "$1" Escape; }
send_line() {
local pane="$1" text="$2"
if [ -n "${SEND_LITERAL_CMD:-}" ]; then
$SEND_LITERAL_CMD "$pane" "$text"
else
tmux send-keys -t "$pane" -l "$text"
sleep 0.2
tmux send-keys -t "$pane" Enter
fi
}
# ── Pure functions ───────────────────────────────────────────────────────────
strip_ansi() {
sed -E $'s/\x1b\\[[0-9;?]*[a-zA-Z]//g; s/\r$//'
}
diff_new_lines() {
local prev="$1" curr="$2"
awk -v prev="$prev" '
BEGIN { n=split(prev, a, "\n"); for (i=1;i<=n;i++) seen[a[i]]=1 }
{ if (!($0 in seen)) print }
' <<<"$curr"
}
classify_reply() {
local r="$1"
[ -z "$r" ] && { echo "NONE"; return; }
local msg
case "$r" in
SILENT|silent) echo "SILENT" ;;
STOP:*) msg="${r#STOP:}"; msg="${msg# }"; echo "STOP $msg" ;;
NOTE:*) msg="${r#NOTE:}"; msg="${msg# }"; echo "NOTE $msg" ;;
WHISPER:*) msg="${r#WHISPER:}"; msg="${msg# }"; echo "NOTE $msg" ;;
*) echo "NOTE $r" ;;
esac
}
# ── Orchestration ────────────────────────────────────────────────────────────
consult_codex() {
local chunk="$1"
local resume_flag="${CODEX_SESSION:-}"
[ -z "$resume_flag" ] && resume_flag="--last"
local prompt
printf -v prompt '%s\n\n<cc-activity>\n%s\n</cc-activity>' \
"$REVIEW_PROMPT" "$(tail -50 <<<"$chunk")"
local reply_file="/tmp/kibitzer-reply.$$"
if [ -n "${CODEX_CMD:-}" ]; then
$CODEX_CMD "$prompt" > "$reply_file" 2>/dev/null
else
(cd "$REPO_DIR" && codex exec resume $resume_flag --full-auto \
-o "$reply_file" "$prompt") >/dev/null 2>&1
fi
local reply=""
[ -f "$reply_file" ] && reply="$(cat "$reply_file")"
rm -f "$reply_file"
echo "$reply"
}
inject() {
local cls="$1"
local msg="${cls#* }"
if [[ "$cls" == STOP* ]]; then
send_escape "$CC_PANE"; sleep 0.2
send_line "$CC_PANE" "# kibitzer STOP: $msg"
else
send_line "$CC_PANE" "# kibitzer: $msg"
fi
}
tick() {
local prev_file="${PREV_FILE:-/tmp/kibitzer.prev.$$}"
local prev=""
[ -f "$prev_file" ] && prev="$(cat "$prev_file")"
local curr; curr="$(capture "$CC_PANE" 2>&1 | strip_ansi)"
echo "$curr" >"$prev_file"
local new; new="$(diff_new_lines "$prev" "$curr")"
TICK_NEW_LINES="$([ -n "$new" ] && wc -l <<<"$new" || echo 0)"
if [ -z "$new" ]; then
TICK_RESULT="no-change"
return 0
fi
local reply; reply="$(consult_codex "$new")"
local cls; cls="$(classify_reply "$reply")"
case "$cls" in
NONE|SILENT) TICK_RESULT="SILENT" ; return 0 ;;
STOP*) TICK_RESULT="STOP" ; inject "$cls" ;;
*) TICK_RESULT="NOTE" ; inject "$cls" ;;
esac
}
# ── Commands ─────────────────────────────────────────────────────────────────
check_cc_pane() {
if capture "$CC_PANE" >/dev/null 2>&1; then
local lines; lines="$(capture "$CC_PANE" 2>/dev/null | wc -l)"
echo " CC pane '$CC_PANE': reachable ($lines lines visible)"
return 0
else
echo " CC pane '$CC_PANE': NOT FOUND — check tmux pane name"
return 1
fi
}
check_codex() {
if command -v codex >/dev/null 2>&1; then
echo " codex CLI: found ($(command -v codex))"
return 0
else
echo " codex CLI: NOT FOUND — install with: npm i -g @openai/codex"
return 1
fi
}
run_loop() {
# kill previous instance if still running
if [ -f "$PIDFILE" ]; then
local old_pid; old_pid="$(cat "$PIDFILE")"
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
echo "kibitzer: killing previous instance (pid $old_pid)"
kill "$old_pid" 2>/dev/null
sleep 0.5
fi
fi
local last_curr="" stable_since=0
export PREV_FILE="/tmp/kibitzer.prev.$$"
: >"$PREV_FILE"
echo $$ > "$PIDFILE"
trap 'rm -f "$PREV_FILE" "$PIDFILE"' EXIT
# ── Startup banner ──
echo "┌─────────────────────────────────────────────┐"
echo "│ kibitzer — silent code review watcher │"
echo "└─────────────────────────────────────────────┘"
echo " pid: $$ pidfile: $PIDFILE"
echo " repo: $REPO_DIR"
echo " CC_PANE=$CC_PANE poll=${POLL_SECS}s idle=${IDLE_SECS}s"
echo ""
echo "── Preflight checks ──"
local checks_ok=true
check_cc_pane || checks_ok=false
check_codex || checks_ok=false
if [ -d "$REPO_DIR/.git" ]; then
echo " repo git: ok"
else
echo " repo git: WARNING — $REPO_DIR is not a git repo (codex requires git)"
checks_ok=false
fi
echo ""
if [ "$checks_ok" = false ]; then
echo "── WARNING: some checks failed — will attempt to run anyway ──"
echo ""
fi
echo "── Watching ($(date +%Y-%m-%d\ %H:%M:%S)) ──"
local tick_count=0 silent_count=0 note_count=0 stop_count=0
TICK_RESULT="" TICK_NEW_LINES=0
while true; do
local curr; curr="$(capture "$CC_PANE" 2>/dev/null | strip_ansi)"
if [ -z "$curr" ]; then
echo "[$(date +%H:%M:%S)] ⚠ CC pane empty or unreachable"
sleep "$POLL_SECS"
continue
fi
if [ "$curr" = "$last_curr" ]; then
if (( stable_since == 0 )); then stable_since=$SECONDS; fi
local idle_elapsed=$(( SECONDS - stable_since ))
if (( idle_elapsed >= IDLE_SECS )); then
local t0=$SECONDS
TICK_RESULT="" TICK_NEW_LINES=0
tick
local elapsed=$(( SECONDS - t0 ))
((tick_count++))
case "$TICK_RESULT" in
SILENT) ((silent_count++)) ;;
NOTE) ((note_count++)) ;;
STOP) ((stop_count++)) ;;
esac
printf '[%s] tick #%-3d %2ds %3d new lines result=%-9s totals: %d silent / %d note / %d stop\n' \
"$(date +%H:%M:%S)" "$tick_count" "$elapsed" "$TICK_NEW_LINES" \
"$TICK_RESULT" "$silent_count" "$note_count" "$stop_count"
stable_since=0
fi
else
last_curr="$curr"; stable_since=0
fi
sleep "$POLL_SECS"
done
}
seed_session() {
echo "Seeding kibitzer Codex session..."
(cd "$REPO_DIR" && codex exec --full-auto "$SEED_PROMPT") 2>&1
echo
echo "Done. Now run: CC_PANE=<pane> REPO_DIR=<repo> $0 run"
}
# ── Tests ────────────────────────────────────────────────────────────────────
assert_eq() {
local name="$1" want="$2" got="$3"
if [ "$want" = "$got" ]; then
echo "ok - $name"
else
echo "FAIL - $name"
echo " want: $(printf %q "$want")"
echo " got: $(printf %q "$got")"
return 1
fi
}
run_tests() {
local fails=0
assert_eq "strip_ansi" "red plain" \
"$(printf '\x1b[31mred\x1b[0m plain' | strip_ansi)" || ((fails++))
assert_eq "diff adds" "c" \
"$(diff_new_lines $'a\nb' $'a\nb\nc')" || ((fails++))
assert_eq "diff same" "" \
"$(diff_new_lines $'a\nb' $'a\nb')" || ((fails++))
assert_eq "cls SILENT" "SILENT" "$(classify_reply "SILENT")" || ((fails++))
assert_eq "cls STOP" "STOP danger" "$(classify_reply "STOP: danger")" || ((fails++))
assert_eq "cls NOTE" "NOTE fyi" "$(classify_reply "NOTE: fyi")" || ((fails++))
assert_eq "cls WHISPER" "NOTE hey" "$(classify_reply "WHISPER: hey")" || ((fails++))
assert_eq "cls bare" "NOTE raw" "$(classify_reply "raw")" || ((fails++))
assert_eq "cls empty" "NONE" "$(classify_reply "")" || ((fails++))
local TMPDIR; TMPDIR="$(mktemp -d)"
local sent_log="$TMPDIR/sent.log"
export CAPTURE_CMD="$(cd "$(dirname "$0")" && pwd)/$(basename "$0") _fake_capture"
export SEND_CMD="$(cd "$(dirname "$0")" && pwd)/$(basename "$0") _fake_send $sent_log"
export SEND_LITERAL_CMD="$(cd "$(dirname "$0")" && pwd)/$(basename "$0") _fake_send $sent_log"
export CODEX_CMD="$(cd "$(dirname "$0")" && pwd)/$(basename "$0") _fake_codex"
export FAKE_CODEX_REPLY="STOP: migration will drop column"
export FAKE_CC_OUT=$'$ edit migrations/0003.sql\nALTER TABLE users DROP email;'
export PREV_FILE="$TMPDIR/prev"
: >"$PREV_FILE"
tick >/dev/null
local sent; sent="$(cat "$sent_log")"
grep -q "Escape" <<<"$sent" \
&& assert_eq "tick Escape on STOP" "1" "1" \
|| { assert_eq "tick Escape on STOP" "1" "0"; ((fails++)); }
grep -q "kibitzer STOP: migration will drop column" <<<"$sent" \
&& assert_eq "tick STOP msg" "1" "1" \
|| { assert_eq "tick STOP msg" "1" "0"; ((fails++)); }
: >"$sent_log"; : >"$PREV_FILE"
export FAKE_CODEX_REPLY="SILENT"
tick >/dev/null
[ ! -s "$sent_log" ] \
&& assert_eq "tick SILENT no inject" "1" "1" \
|| { assert_eq "tick SILENT no inject" "1" "0"; ((fails++)); }
rm -rf "$TMPDIR"
echo
(( fails == 0 )) && echo "All tests passed." || echo "$fails test(s) failed."
return "$fails"
}
# ── Test fakes (spawned as subprocesses) ─────────────────────────────────────
_fake_capture() { printf '%s\n' "${FAKE_CC_OUT:-}"; }
_fake_send() { local log="$1"; shift; printf 'SEND pane=%s args=%s\n' "$1" "${*:2}" >>"$log"; }
_fake_codex() { echo "${FAKE_CODEX_REPLY:-SILENT}"; }
# ── Entry ────────────────────────────────────────────────────────────────────
case "${1:-run}" in
run) run_loop ;;
once) tick ;;
test) run_tests ;;
seed) seed_session ;;
_fake_capture) shift; _fake_capture "$@" ;;
_fake_send) shift; _fake_send "$@" ;;
_fake_codex) shift; _fake_codex "$@" ;;
*) echo "usage: $0 {run|once|test|seed}" >&2; exit 2 ;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment