
Claude Code Wingman
- 15 installs
- 638 repo stars
- Updated March 7, 2026
- sundial-org/awesome-openclaw-skills
Helps with ai & agent building tasks during AI-assisted development.
About
claude-code-wingman is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- claude-code-wingman
- AI & Agent Building
- AI-coding skill
Claude Code Wingman by the numbers
- 15 all-time installs (skills.sh)
- Ranked #11,165 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/sundial-org/awesome-openclaw-skills --skill claude-code-wingmanAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 638 |
| Last updated | March 7, 2026 |
| Repository | sundial-org/awesome-openclaw-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Claude Code Wingman
Automate Claude Code sessions from Clawdbot - leverage free/work Claude Code API while keeping your Anthropic budget for conversations.
GitHub: https://github.com/yossiovadia/claude-code-wingman
What It Does
Spawns Claude Code in tmux sessions with automatic approval of permission prompts. Perfect for when you have free/work Claude Code access but limited Anthropic API budget.
Cost Comparison:
- Without: Clawdbot does all coding → uses your $20/month API
- With: Clawdbot spawns Claude Code → uses work's free API ✅
Installation
The skill references the standalone repo. Install it once:
cd ~/code
git clone https://github.com/yossiovadia/claude-code-wingman.git
cd claude-code-wingman
chmod +x *.shUsage from Clawdbot
When a user asks for coding work, spawn Claude Code:
~/code/claude-code-wingman/claude-wingman.sh \
--session <session-name> \
--workdir <project-directory> \
--prompt "<task description>"Example Patterns
User: "Fix the bug in api.py"
Clawdbot Response:
Spawning Claude Code for this...
bash:~/code/claude-code-wingman/claude-wingman.sh \
--session vsr-bug-fix \
--workdir ~/code/semantic-router \
--prompt "Fix the bug in src/api.py - add proper error handling for null responses"Then report:
- Session name (so user can attach)
- Monitor command
- Auto-approver is running
User: "What's the status?"
Clawdbot: Capture tmux output and summarize:
tmux capture-pane -t vsr-bug-fix -p -S -50Commands
Spawn Session
~/code/claude-code-wingman/claude-wingman.sh \
--session <name> \
--workdir <dir> \
--prompt "<task>"Monitor Progress
tmux capture-pane -t <session-name> -p -S -100View Auto-Approver Log
cat /tmp/auto-approver-<session-name>.logKill Session
tmux kill-session -t <session-name>List All Sessions
tmux ls | grep claude-autoWorkflow
1. User requests coding work (fix bug, add feature, refactor, etc.) 2. Clawdbot spawns Claude Code via wingman script 3. Auto-approver handles permissions in background 4. Clawdbot monitors and reports progress 5. User can attach anytime to see/control directly 6. Claude Code does the work on work's API ✅
Trust Prompt (First Time Only)
When running in a new directory, Claude Code asks:
"Do you trust the files in this folder?"
First run: User must attach and approve (press Enter). After that, it's automatic.
Handle it:
User, Claude Code needs you to approve the folder trust (one-time). Please run:
tmux attach -t <session-name>
Press Enter to approve, then Ctrl+B followed by D to detach.Best Practices
When to Use Wingman
✅ Use wingman for:
- Heavy code generation/refactoring
- Multi-file changes
- Long-running tasks
- Repetitive coding work
❌ Don't use wingman for:
- Quick file reads
- Simple edits
- When conversation is needed
- Planning/design discussions
Session Naming
Use descriptive names:
vsr-issue-1131- specific issue workvsr-feature-auth- feature developmentproject-bugfix-X- bug fixes
Troubleshooting
Prompt Not Submitting
The wingman sends Enter twice with delays. If stuck, user can attach and press Enter manually.
Auto-Approver Not Working
Check logs: cat /tmp/auto-approver-<session-name>.log
Should see: "Approval prompt detected! Navigating to option 2..."
Session Already Exists
Kill it: tmux kill-session -t <name>
Advanced: Update Memory
After successful tasks, update TOOLS.md:
### Recent Claude Code Sessions
- 2026-01-26: VSR AWS check - verified vLLM server running ✅
- Session pattern: vsr-* for semantic-router workPro Tips
- Parallel sessions: Run multiple tasks simultaneously in different sessions
- Name consistently: Use project prefixes (vsr-, myapp-, etc.)
- Monitor periodically: Check progress every few minutes
- Let it finish: Don't kill sessions early, let Claude Code complete
---
Remember: This skill saves API costs by using free work Claude Code for heavy lifting, keeping your Anthropic budget for conversations.
#!/bin/bash
# Auto-Approver: Monitors tmux session and auto-approves Claude Code prompts
set -uo pipefail
SESSION_NAME="$1"
if [ -z "$SESSION_NAME" ]; then
echo "Usage: $0 <tmux-session-name>"
exit 1
fi
echo "[Auto-Approver] Monitoring session: $SESSION_NAME"
while true; do
# Capture current pane content
OUTPUT=$(tmux capture-pane -t "$SESSION_NAME" -p 2>/dev/null)
# Check if session still exists
if [ $? -ne 0 ]; then
echo "[Auto-Approver] Session ended. Exiting."
exit 0
fi
# Look for "Do you trust..." prompt (folder trust)
if echo "$OUTPUT" | grep -q "Do you trust"; then
echo "[Auto-Approver] Trust prompt detected! Approving folder trust..."
# Just press Enter to confirm option 1 (Yes, proceed)
tmux send-keys -t "$SESSION_NAME" Enter
sleep 2
# Look for approval prompt (any "Do you want..." question)
elif echo "$OUTPUT" | grep -q "Do you want"; then
echo "[Auto-Approver] Approval prompt detected! Navigating to option 2 and confirming..."
# Option 2 is typically "Yes, and allow for session/project"
# Navigate down one option (from 1 to 2) and press Enter
tmux send-keys -t "$SESSION_NAME" Down Enter
sleep 2 # Give it time to process
fi
sleep 1 # Check every second
done
Changelog
2026-01-26 - Initial Release
✅ Completed Features
- Auto-approver script - Detects and handles Claude Code approval prompts
- Handles "Do you want..." prompts
- Handles "Do you trust..." folder prompts
- Uses arrow keys + Enter for TUI navigation
- Wingman script - Complete automation wrapper
- Spawns Claude Code in named tmux sessions
- Starts auto-approver in background
- Reliable prompt submission (double-tap Enter)
- Trust prompt detection and handling
- Optional monitoring and wait modes
✅ Real-World Testing
- Test 1: Simple file creation (test2.txt, test3.txt, test5.txt) ✅
- Test 2: AWS VM connectivity check (VSR project) ✅
- SSH connection verified
- vLLM server status checked
- API health confirmed
- Multiple SSH commands auto-approved without human intervention
🎯 Proven Benefits
- Cost savings: Uses work's free Claude Code API instead of $20/month Anthropic budget
- Zero intervention: Runs completely hands-off after initial folder trust
- Parallel sessions: Can run multiple tasks simultaneously
- Full transparency: Can attach to any session anytime to see progress
🐛 Known Issues
- Trust prompt appears first time in new directory (one-time approval needed)
- Enter key timing occasionally requires double-tap
- Auto-approver doesn't log when prompts are handled instantly
📝 Next Steps
- [ ] Package as Clawdbot skill
- [ ] Push to GitHub
- [ ] Add more examples
- [ ] Improve trust prompt automation
#!/bin/bash
# Check for pending approval requests and notify
APPROVAL_DIR="/tmp/claude-approvals"
if [ ! -d "$APPROVAL_DIR" ]; then
exit 0
fi
# Find all pending files
for PENDING in "$APPROVAL_DIR"/*.pending; do
[ -e "$PENDING" ] || continue
SESSION=$(basename "$PENDING" .pending)
RESPONSE_FILE="$APPROVAL_DIR/${SESSION}.response"
# Skip if already has response
[ -f "$RESPONSE_FILE" ] && continue
echo "⚠️ Approval needed for session: $SESSION"
echo ""
cat "$PENDING"
echo ""
echo "Respond with: ~/code/claude-code-wingman/respond-approval.sh $SESSION <yes|always|no>"
done
#!/bin/bash
# Claude Code Wingman - Complete wrapper for automated Claude Code sessions
set -e
# Check dependencies
if ! command -v tmux &> /dev/null; then
echo "Error: tmux is not installed."
echo ""
echo "Install tmux:"
echo " macOS: brew install tmux"
echo " Ubuntu: sudo apt-get install tmux"
echo " Fedora: sudo dnf install tmux"
echo ""
exit 1
fi
if ! command -v claude &> /dev/null; then
echo "Error: Claude Code CLI is not installed."
echo ""
echo "Get Claude Code from: https://claude.ai/code"
echo ""
exit 1
fi
# Configuration
SESSION_PREFIX="claude-auto"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AUTO_APPROVER="$SCRIPT_DIR/auto-approver.sh"
# Usage
usage() {
echo "Usage: $0 [options]"
echo ""
echo "Options:"
echo " --session <name> Session name (default: auto-generated)"
echo " --workdir <path> Working directory (default: current dir)"
echo " --prompt <text> Prompt to send to Claude Code"
echo " --monitor Monitor session in real-time (blocks)"
echo " --wait Wait for completion"
echo " --auto Auto-approve all prompts (default: interactive)"
echo ""
echo "Examples:"
echo " $0 --workdir ~/code/myproject --prompt 'Add error handling to api.py'"
echo " $0 --session vsr-123 --prompt 'Fix the bug in line 42' --wait"
exit 1
}
# Parse arguments
SESSION_NAME=""
WORKDIR="$(pwd)"
PROMPT=""
MONITOR=false
WAIT=false
INTERACTIVE=true # Default to interactive mode for safety
while [[ $# -gt 0 ]]; do
case $1 in
--session)
SESSION_NAME="$2"
shift 2
;;
--workdir)
WORKDIR="$2"
shift 2
;;
--prompt)
PROMPT="$2"
shift 2
;;
--monitor)
MONITOR=true
shift
;;
--wait)
WAIT=true
shift
;;
--auto)
INTERACTIVE=false
shift
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1"
usage
;;
esac
done
# Validate
if [ -z "$PROMPT" ]; then
echo "Error: --prompt is required"
usage
fi
if [ ! -d "$WORKDIR" ]; then
echo "Error: Working directory does not exist: $WORKDIR"
exit 1
fi
# Generate session name if not provided
if [ -z "$SESSION_NAME" ]; then
SESSION_NAME="${SESSION_PREFIX}-$(date +%s)"
fi
# Validate session name (alphanumeric, underscore, hyphen only)
if [[ ! "$SESSION_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "Error: Invalid session name. Use only alphanumeric characters, underscores, and hyphens."
exit 1
fi
echo "[Wingman] Starting Claude Code session: $SESSION_NAME"
echo "[Wingman] Working directory: $WORKDIR"
echo "[Wingman] Prompt: $PROMPT"
echo ""
# Clean up any existing session
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
# Step 1: Create tmux session
echo "[Wingman] Creating tmux session..."
tmux new-session -d -s "$SESSION_NAME" -c "$WORKDIR"
# Step 2: Start approver in background
if [ "$INTERACTIVE" = true ]; then
echo "[Wingman] Starting interactive approver..."
LOG_FILE="/tmp/interactive-approver-${SESSION_NAME}.log"
INTERACTIVE_APPROVER="$SCRIPT_DIR/interactive-approver.sh"
touch "$LOG_FILE" && chmod 600 "$LOG_FILE"
"$INTERACTIVE_APPROVER" "$SESSION_NAME" > "$LOG_FILE" 2>&1 &
APPROVER_PID=$!
echo "[Wingman] Interactive approver running (PID: $APPROVER_PID)"
echo "[Wingman] You'll be notified when approval is needed"
else
echo "[Wingman] Starting auto-approver..."
LOG_FILE="/tmp/auto-approver-${SESSION_NAME}.log"
touch "$LOG_FILE" && chmod 600 "$LOG_FILE"
"$AUTO_APPROVER" "$SESSION_NAME" > "$LOG_FILE" 2>&1 &
APPROVER_PID=$!
echo "[Wingman] Auto-approver running (PID: $APPROVER_PID)"
fi
# Step 3: Start Claude Code
echo "[Wingman] Launching Claude Code..."
tmux send-keys -t "$SESSION_NAME" "claude" C-m
# Wait for Claude Code to initialize
echo "[Wingman] Waiting for Claude Code to initialize..."
sleep 5
# Check for trust prompt and handle it
echo "[Wingman] Checking for trust prompt..."
for i in {1..5}; do
OUTPUT=$(tmux capture-pane -t "$SESSION_NAME" -p)
if echo "$OUTPUT" | grep -q "Do you trust"; then
echo "[Wingman] Trust prompt detected, approving..."
tmux send-keys -t "$SESSION_NAME" Enter
sleep 2
break
fi
sleep 1
done
# Step 4: Send prompt
echo "[Wingman] Sending prompt..."
tmux send-keys -t "$SESSION_NAME" "$PROMPT"
sleep 1
# Send Enter explicitly (C-m might not always work reliably in tmux)
tmux send-keys -t "$SESSION_NAME" C-m
sleep 1
# Double-tap Enter to ensure it goes through
tmux send-keys -t "$SESSION_NAME" C-m
sleep 2
echo ""
echo "[Wingman] Session started successfully!"
echo ""
echo "Commands:"
echo " Attach: tmux attach -t $SESSION_NAME"
echo " Monitor: tmux capture-pane -t $SESSION_NAME -p"
echo " Logs: cat $LOG_FILE"
echo " Kill: tmux kill-session -t $SESSION_NAME"
echo ""
# Monitor mode
if [ "$MONITOR" = true ]; then
echo "[Wingman] Monitoring session (Ctrl+C to stop monitoring, session continues)..."
echo ""
while tmux has-session -t "$SESSION_NAME" 2>/dev/null; do
clear
echo "=== Session: $SESSION_NAME ==="
tmux capture-pane -t "$SESSION_NAME" -p
echo ""
echo "=== Auto-Approver Log ==="
tail -5 "$LOG_FILE" 2>/dev/null || echo "(no log yet)"
sleep 2
done
echo "[Wingman] Session ended."
fi
# Wait mode
if [ "$WAIT" = true ] && [ "$MONITOR" = false ]; then
echo "[Wingman] Waiting for session to complete..."
while tmux has-session -t "$SESSION_NAME" 2>/dev/null; do
sleep 5
done
echo "[Wingman] Session completed."
echo ""
echo "=== Auto-Approver Log ==="
cat "$LOG_FILE"
fi
Clawdbot Skill: Claude Code Wingman
This directory contains the Clawdbot skill wrapper for the Claude Code Wingman.
Installation for Clawdbot
1. Clone the main repo (if not already):
cd ~/code
git clone https://github.com/yossiovadia/claude-code-wingman.git2. Copy this skill to your Clawdbot skills directory:
cp -r ~/code/claude-code-wingman/clawdbot-skill ~/.clawdbot/skills/claude-code-wingmanOr create a symlink:
ln -s ~/code/claude-code-wingman/clawdbot-skill ~/.clawdbot/skills/claude-code-wingman3. Restart Clawdbot:
clawdbot gateway restartUsage
See SKILL.md for full documentation on how Clawdbot should use this skill.
Requirements
- tmux
- Claude Code CLI (
claude) - Bash
- Main wingman scripts in
~/code/claude-code-wingman/
#!/bin/bash
# Claude Code Session Dashboard
# Shows all active Claude Code sessions with their status
echo "╭─── Claude Code Sessions ───────────────────────────────────────────╮"
echo "│"
# Get all tmux sessions
SESSIONS=$(tmux list-sessions -F "#{session_name}" 2>/dev/null)
if [ -z "$SESSIONS" ]; then
echo "│ No active sessions"
else
for SESSION in $SESSIONS; do
# Get session creation time
CREATED=$(tmux display-message -t "$SESSION" -p "#{session_created}")
CREATED_DATE=$(date -r "$CREATED" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "Unknown")
# Check if it's a Claude Code session (look for "claude" in pane content)
OUTPUT=$(tmux capture-pane -t "$SESSION" -p 2>/dev/null)
IS_CLAUDE=false
if echo "$OUTPUT" | grep -q "Claude Code"; then
IS_CLAUDE=true
fi
# Determine status
STATUS="🟢 Active"
if echo "$OUTPUT" | grep -q "Do you want"; then
STATUS="⏸️ Waiting for approval"
elif echo "$OUTPUT" | grep -q "error"; then
STATUS="❌ Error"
elif echo "$OUTPUT" | grep -q "✓"; then
STATUS="✅ Task complete"
fi
# Get approver log if exists
AUTO_LOG="/tmp/auto-approver-${SESSION}.log"
INTERACTIVE_LOG="/tmp/interactive-approver-${SESSION}.log"
APPROVALS=0
if [ -f "$AUTO_LOG" ]; then
APPROVALS=$(grep -c "Approval prompt detected" "$AUTO_LOG" 2>/dev/null || echo "0")
APPROVER_TYPE="🤖 Auto"
elif [ -f "$INTERACTIVE_LOG" ]; then
APPROVALS=$(grep -c "Approval prompt detected" "$INTERACTIVE_LOG" 2>/dev/null || echo "0")
APPROVER_TYPE="👤 Interactive"
else
APPROVER_TYPE="⚠️ None"
fi
# Print session info
if [ "$IS_CLAUDE" = true ]; then
echo "│ 📋 $SESSION"
echo "│ Status: $STATUS"
echo "│ Created: $CREATED_DATE"
echo "│ Approver: $APPROVER_TYPE (${APPROVALS} approvals)"
echo "│ Commands:"
echo "│ Attach: tmux attach -t $SESSION"
echo "│ Monitor: tmux capture-pane -t $SESSION -p"
echo "│ Kill: tmux kill-session -t $SESSION"
echo "│"
fi
done
fi
echo "╰────────────────────────────────────────────────────────────────────╯"
echo ""
echo "💡 Tips:"
echo " • Use --auto flag for automated approvals"
echo " • Check logs: cat /tmp/*-approver-<session>.log"
echo " • Monitor live: watch -n 2 ./dashboard.sh"
#!/bin/bash
# Interactive Approver: Detects prompts and notifies user for approval
set -uo pipefail
SESSION_NAME="$1"
APPROVAL_DIR="/tmp/claude-approvals"
if [ -z "$SESSION_NAME" ]; then
echo "Usage: $0 <tmux-session-name>"
exit 1
fi
mkdir -p "$APPROVAL_DIR"
PENDING_FILE="$APPROVAL_DIR/${SESSION_NAME}.pending"
RESPONSE_FILE="$APPROVAL_DIR/${SESSION_NAME}.response"
echo "[Interactive-Approver] Monitoring session: $SESSION_NAME"
echo "[Interactive-Approver] Approval dir: $APPROVAL_DIR"
while true; do
# Capture current pane content
OUTPUT=$(tmux capture-pane -t "$SESSION_NAME" -p 2>/dev/null)
# Check if session still exists
if [ $? -ne 0 ]; then
echo "[Interactive-Approver] Session ended. Exiting."
rm -f "$PENDING_FILE" "$RESPONSE_FILE"
exit 0
fi
# Look for "Do you trust..." prompt (folder trust)
if echo "$OUTPUT" | grep -q "Do you trust"; then
# Auto-approve trust prompts (one-time, safe)
echo "[Interactive-Approver] Trust prompt detected, auto-approving..."
tmux send-keys -t "$SESSION_NAME" Enter
sleep 2
continue
fi
# Look for approval prompt (any "Do you want..." question)
if echo "$OUTPUT" | grep -q "Do you want"; then
# Check if already waiting for response
if [ -f "$PENDING_FILE" ]; then
# Check for user response
if [ -f "$RESPONSE_FILE" ]; then
RESPONSE=$(cat "$RESPONSE_FILE")
echo "[Interactive-Approver] User responded: $RESPONSE"
case "$RESPONSE" in
yes|1)
echo "[Interactive-Approver] Approving (option 1)..."
tmux send-keys -t "$SESSION_NAME" Enter
;;
always|2)
echo "[Interactive-Approver] Approving always (option 2)..."
tmux send-keys -t "$SESSION_NAME" Down Enter
;;
no|3)
echo "[Interactive-Approver] Declining..."
tmux send-keys -t "$SESSION_NAME" Down Down Enter
;;
*)
echo "[Interactive-Approver] Unknown response, defaulting to decline"
tmux send-keys -t "$SESSION_NAME" Down Down Enter
;;
esac
rm -f "$PENDING_FILE" "$RESPONSE_FILE"
sleep 2
fi
else
# New prompt detected, notify user
echo "[Interactive-Approver] Approval prompt detected! Waiting for user decision..."
# Extract the prompt question
QUESTION=$(echo "$OUTPUT" | grep -A 10 "Do you want" | head -20)
# Write to pending file
cat > "$PENDING_FILE" << EOF
Session: $SESSION_NAME
Timestamp: $(date)
Prompt:
$QUESTION
To respond, write one of these to: $RESPONSE_FILE
yes - Approve this once (option 1)
always - Approve and don't ask again for this project (option 2)
no - Decline (option 3)
EOF
echo "[Interactive-Approver] Pending approval written to: $PENDING_FILE"
fi
fi
sleep 1 # Check every second
done
#!/bin/bash
# approval-respond.sh - Send an approval response to a Claude Code session
# Usage: approval-respond.sh <session-name> <response>
# Response can be: y, n, always, never, approve, deny
set -e
SESSION_NAME="$1"
RESPONSE="$2"
if [ -z "$SESSION_NAME" ] || [ -z "$RESPONSE" ]; then
echo "Usage: $0 <session-name> <response>" >&2
echo "Response options: y, n, always, never, approve, deny" >&2
exit 1
fi
# Check if session exists
if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
echo "Error: tmux session '$SESSION_NAME' does not exist" >&2
exit 1
fi
# Normalize response
case "$RESPONSE" in
y|yes|approve)
# For menu-style prompts, select option 1
tmux send-keys -t "$SESSION_NAME" C-m
;;
n|no|deny)
# For menu-style prompts, select option 3
tmux send-keys -t "$SESSION_NAME" Down Down C-m
;;
always|all)
# For menu-style prompts, select option 2 (allow all)
tmux send-keys -t "$SESSION_NAME" Down C-m
;;
never)
# Send 'never' for traditional prompts
tmux send-keys -t "$SESSION_NAME" "never" C-m
;;
*)
echo "Error: Invalid response '$RESPONSE'" >&2
echo "Valid options: y, yes, approve, n, no, deny, always, all, never" >&2
exit 1
;;
esac
echo "✓ Sent approval response to session '$SESSION_NAME': $NORMALIZED"
#!/bin/bash
# handle-approval.sh - Handle approval commands from WhatsApp
# Usage: handle-approval.sh <action> <session-name>
# Actions: approve, always, deny
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ACTION="$1"
SESSION="$2"
if [ -z "$ACTION" ] || [ -z "$SESSION" ]; then
echo "Usage: $0 <approve|always|deny> <session-name>" >&2
exit 1
fi
# Normalize action
case "$ACTION" in
approve|yes|y|1)
RESPONSE="y"
ACTION_DESC="approved (once)"
;;
always|all|2)
RESPONSE="always"
ACTION_DESC="approved (always)"
;;
deny|no|n|reject|3)
RESPONSE="n"
ACTION_DESC="denied"
;;
*)
echo "Error: Unknown action '$ACTION'" >&2
echo "Valid actions: approve, always, deny" >&2
exit 1
;;
esac
# Check if session exists
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
echo "Error: Session '$SESSION' not found" >&2
echo "Active sessions:"
tmux ls 2>/dev/null || echo " (none)"
exit 1
fi
# Send the response
"$SCRIPT_DIR/approval-respond.sh" "$SESSION" "$RESPONSE"
echo "✓ Session '$SESSION' $ACTION_DESC"
Helper Library for Claude Code Orchestration
This directory contains modular helper scripts for managing Claude Code sessions in tmux.
Scripts
session-send.sh
Send a command to an existing Claude Code session.
Usage:
./lib/session-send.sh <session-name> <command> [options]Options:
--force, -f- Send without checking session state--timeout, -t N- Wait up to N seconds for idle state (default: 30)--quiet, -q- Suppress output messages
Example:
./lib/session-send.sh my-session "Read the README file"
./lib/session-send.sh my-session "Fix the bug" --timeout 60
./lib/session-send.sh my-session "Quick task" --forceFeatures:
- Validates session exists
- Checks session state before sending (waits for idle, fails on waiting_approval/error)
- Smart retry: sends Enter again if session remains idle after first attempt
- Configurable timeout for waiting on busy sessions
- Force mode for bypassing state checks
---
approval-respond.sh
Send an approval response to a Claude Code session waiting for approval.
Usage:
./lib/approval-respond.sh <session-name> <response>Response options:
y,yes,approve- Approve this one actionalways,all- Approve all actions for this sessionn,no,deny- Deny this actionnever- Never approve (for traditional prompts)
Example:
./lib/approval-respond.sh my-session alwaysFeatures:
- Handles both menu-style and traditional approval prompts
- Uses arrow key navigation for menu selections
- Validates session exists
- Normalizes various response formats
---
session-status.sh
Get the current status of a Claude Code session.
Usage:
./lib/session-status.sh <session-name> [--json]Example:
./lib/session-status.sh my-session
./lib/session-status.sh my-session --jsonOutput states:
waiting_approval- Session is waiting for user approvalidle- Session is ready for inputerror- Session has encountered an errorworking- Session is actively processingunknown- Cannot determine state
JSON output:
{
"session": "my-session",
"status": "waiting_approval",
"details": "⏺ Write(test.txt)"
}---
Architecture
These helpers follow the Unix philosophy:
- Do one thing well - Each script has a single, focused purpose
- Composable - Can be used standalone or combined
- Simple - Clear interfaces, easy to understand
- Testable - Each can be tested independently
These scripts are used by:
- The main
wingman.shorchestrator - The heartbeat monitoring system
- Direct manual invocation
- Future automation tools
Integration
The main wingman.sh script delegates to these helpers for session management operations, providing a clean user-facing interface while keeping the internals modular and maintainable.
#!/bin/bash
# send-notification.sh - Send WhatsApp notification via Clawdbot webhook
# Usage: send-notification.sh "Your message here"
set -e
MESSAGE="${1:-}"
CLAWDBOT_CONFIG="${CLAWDBOT_CONFIG:-$HOME/.clawdbot/clawdbot.json}"
WEBHOOK_URL="${CLAWDBOT_WEBHOOK_URL:-http://127.0.0.1:18789/hooks/agent}"
# Get phone from env or clawdbot config
if [ -n "$CLAWDBOT_PHONE" ]; then
PHONE="$CLAWDBOT_PHONE"
elif [ -f "$CLAWDBOT_CONFIG" ]; then
PHONE=$(jq -r '.channels.whatsapp.allowFrom[0] // empty' "$CLAWDBOT_CONFIG" 2>/dev/null)
fi
if [ -z "$PHONE" ]; then
echo "Error: No phone number found. Set CLAWDBOT_PHONE or configure allowFrom in clawdbot.json" >&2
exit 1
fi
# Get webhook token from env or clawdbot config
if [ -n "$CLAWDBOT_WEBHOOK_TOKEN" ]; then
WEBHOOK_TOKEN="$CLAWDBOT_WEBHOOK_TOKEN"
elif [ -f "$CLAWDBOT_CONFIG" ]; then
WEBHOOK_TOKEN=$(jq -r '.hooks.token // empty' "$CLAWDBOT_CONFIG" 2>/dev/null)
fi
if [ -z "$WEBHOOK_TOKEN" ]; then
echo "Error: No webhook token found. Set CLAWDBOT_WEBHOOK_TOKEN or configure hooks.token in clawdbot.json" >&2
exit 1
fi
if [ -z "$MESSAGE" ]; then
echo "Usage: $0 <message>" >&2
exit 1
fi
# Escape message for JSON
ESCAPED_MESSAGE=$(echo "$MESSAGE" | jq -Rs .)
curl -s -X POST "$WEBHOOK_URL" \
-H "Authorization: Bearer $WEBHOOK_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"message\": $ESCAPED_MESSAGE,
\"name\": \"OrchestratorMonitor\",
\"deliver\": true,
\"channel\": \"whatsapp\",
\"to\": \"$PHONE\"
}"
#!/bin/bash
# session-send.sh - Send a command to an existing Claude Code tmux session
# Usage: session-send.sh <session-name> <command> [options]
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SESSION_STATUS="$SCRIPT_DIR/session-status.sh"
# Defaults
SESSION_NAME=""
COMMAND=""
FORCE=false
WAIT_TIMEOUT=30 # seconds to wait for idle state
QUIET=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--force|-f)
FORCE=true
shift
;;
--timeout|-t)
WAIT_TIMEOUT="$2"
shift 2
;;
--quiet|-q)
QUIET=true
shift
;;
--help|-h)
echo "Usage: $0 <session-name> <command> [options]"
echo ""
echo "Options:"
echo " --force, -f Send without checking session state"
echo " --timeout, -t N Wait up to N seconds for idle state (default: 30)"
echo " --quiet, -q Suppress output messages"
echo ""
echo "Example: $0 my-session 'Read the README file'"
exit 0
;;
*)
if [ -z "$SESSION_NAME" ]; then
SESSION_NAME="$1"
elif [ -z "$COMMAND" ]; then
COMMAND="$1"
else
echo "Error: Unknown argument: $1" >&2
exit 1
fi
shift
;;
esac
done
if [ -z "$SESSION_NAME" ] || [ -z "$COMMAND" ]; then
echo "Usage: $0 <session-name> <command> [options]" >&2
echo "Example: $0 my-session 'Read the README file'" >&2
echo "Use --help for more options" >&2
exit 1
fi
# Helper function for output
log() {
if [ "$QUIET" = false ]; then
echo "$@"
fi
}
# Check if session exists
if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
echo "Error: tmux session '$SESSION_NAME' does not exist" >&2
exit 1
fi
# Check session status before sending (unless --force)
if [ "$FORCE" = false ]; then
log "Checking session state..."
ELAPSED=0
while [ $ELAPSED -lt $WAIT_TIMEOUT ]; do
# Get session status
STATUS_JSON=$("$SESSION_STATUS" "$SESSION_NAME" --json 2>/dev/null || echo '{"status":"error"}')
STATUS=$(echo "$STATUS_JSON" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
case "$STATUS" in
idle)
log "Session is ready (idle)"
break
;;
waiting_approval)
echo "Error: Session is waiting for approval. Approve it first or use --force" >&2
exit 1
;;
working)
log "Session is working, waiting... ($ELAPSED/${WAIT_TIMEOUT}s)"
sleep 2
ELAPSED=$((ELAPSED + 2))
;;
error)
echo "Error: Session is in error state" >&2
exit 1
;;
*)
# Unknown state - wait a bit and retry
log "Session state: $STATUS, waiting... ($ELAPSED/${WAIT_TIMEOUT}s)"
sleep 2
ELAPSED=$((ELAPSED + 2))
;;
esac
done
# Final check after timeout
if [ $ELAPSED -ge $WAIT_TIMEOUT ]; then
STATUS_JSON=$("$SESSION_STATUS" "$SESSION_NAME" --json 2>/dev/null || echo '{"status":"error"}')
STATUS=$(echo "$STATUS_JSON" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
if [ "$STATUS" != "idle" ]; then
echo "Error: Timed out waiting for session to become idle (current: $STATUS)" >&2
echo "Use --force to send anyway, or --timeout N to wait longer" >&2
exit 1
fi
fi
fi
# Send the command
log "Sending command..."
tmux send-keys -t "$SESSION_NAME" "$COMMAND"
sleep 0.5
# Send Enter once
tmux send-keys -t "$SESSION_NAME" C-m
# Verify the command was sent by checking if state changed
sleep 1
if [ "$FORCE" = false ]; then
STATUS_JSON=$("$SESSION_STATUS" "$SESSION_NAME" --json 2>/dev/null || echo '{"status":"unknown"}')
STATUS=$(echo "$STATUS_JSON" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
if [ "$STATUS" = "idle" ]; then
# Still idle - command might not have been received, try Enter again
log "Session still idle, sending Enter again..."
tmux send-keys -t "$SESSION_NAME" C-m
sleep 1
fi
fi
log "Sent command to session '$SESSION_NAME': $COMMAND"
#!/bin/bash
# session-status.sh - Get the current status of a Claude Code session
# Usage: session-status.sh <session-name> [--json]
# session-status.sh --all [--json]
set -e
SESSION_NAME="$1"
JSON_OUTPUT=false
CHECK_ALL=false
# Parse arguments
for arg in "$@"; do
if [ "$arg" = "--json" ]; then
JSON_OUTPUT=true
elif [ "$arg" = "--all" ]; then
CHECK_ALL=true
fi
done
# If --all flag, check all tmux sessions
if [ "$CHECK_ALL" = true ]; then
# Get all tmux sessions
ALL_SESSIONS=$(tmux list-sessions -F "#{session_name}" 2>/dev/null || echo "")
if [ -z "$ALL_SESSIONS" ]; then
if [ "$JSON_OUTPUT" = true ]; then
echo '[]'
else
echo "No tmux sessions found"
fi
exit 0
fi
# Check each session and build JSON array
if [ "$JSON_OUTPUT" = true ]; then
echo "["
FIRST=true
while IFS= read -r session; do
if [ "$FIRST" = false ]; then
echo ","
fi
FIRST=false
"$0" "$session" --json | tr -d '\n'
done <<< "$ALL_SESSIONS"
echo ""
echo "]"
else
while IFS= read -r session; do
"$0" "$session" --json | jq -r '"Session: \(.session)\nStatus: \(.status)\nDetails: \(.details)\n"'
done <<< "$ALL_SESSIONS"
fi
exit 0
fi
# Single session mode
if [ -z "$SESSION_NAME" ]; then
echo "Usage: $0 <session-name> [--json]" >&2
echo " $0 --all [--json]" >&2
exit 1
fi
# Check if session exists
if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
if [ "$JSON_OUTPUT" = true ]; then
echo '{"status":"not_found","session":"'$SESSION_NAME'"}'
else
echo "Error: tmux session '$SESSION_NAME' does not exist" >&2
fi
exit 1
fi
# Capture recent output from the session
OUTPUT=$(tmux capture-pane -t "$SESSION_NAME" -p | tail -20)
# Detect state
STATUS="unknown"
DETAILS=""
if echo "$OUTPUT" | grep -qE "Allow this tool call|y/n/always|Do you want to|❯ 1\. Yes"; then
STATUS="waiting_approval"
# Try to extract what's being approved
DETAILS=$(echo "$OUTPUT" | grep -E "Create file|Write\(|Edit\(|Bash\(" | head -1 || echo "Approval needed")
elif echo "$OUTPUT" | grep -q "❯"; then
STATUS="idle"
DETAILS="Ready for input"
elif echo "$OUTPUT" | grep -qE "Error:|Failed:|Exception:"; then
STATUS="error"
DETAILS=$(echo "$OUTPUT" | grep -E "Error:|Failed:|Exception:" | tail -1 || echo "")
elif echo "$OUTPUT" | grep -qE "Reading|Writing|Executing|Running"; then
STATUS="working"
DETAILS=$(echo "$OUTPUT" | tail -3 | head -1 || echo "")
else
STATUS="unknown"
DETAILS="Unable to determine state"
fi
# Output
if [ "$JSON_OUTPUT" = true ]; then
echo "{\"session\":\"$SESSION_NAME\",\"status\":\"$STATUS\",\"details\":$(echo "$DETAILS" | jq -Rs .)}"
else
echo "Session: $SESSION_NAME"
echo "Status: $STATUS"
if [ -n "$DETAILS" ]; then
echo "Details: $DETAILS"
fi
fi
#!/bin/bash
# master-monitor.sh - Master daemon that monitors all Claude Code sessions
# and sends WhatsApp notifications when approval is needed
#
# Usage: ./master-monitor.sh [--poll-interval SECONDS] [--reminder-interval SECONDS]
#
# Environment variables (optional - reads from ~/.clawdbot/clawdbot.json if not set):
# CLAWDBOT_PHONE - Phone number to notify (default: from clawdbot config)
# CLAWDBOT_WEBHOOK_TOKEN - Webhook auth token (default: from clawdbot config)
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LIB_DIR="$SCRIPT_DIR/lib"
# Configuration
POLL_INTERVAL="${POLL_INTERVAL:-10}"
REMINDER_INTERVAL="${REMINDER_INTERVAL:-300}"
# File paths
STATE_DIR="/tmp/claude-orchestrator"
LOG_FILE="$STATE_DIR/master-monitor.log"
PID_FILE="$STATE_DIR/master-monitor.pid"
NOTIFY_STATE_DIR="$STATE_DIR/notify-state"
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--poll-interval)
POLL_INTERVAL="$2"
shift 2
;;
--reminder-interval)
REMINDER_INTERVAL="$2"
shift 2
;;
--help|-h)
echo "Usage: $0 [--poll-interval SECONDS] [--reminder-interval SECONDS]"
echo ""
echo "Options:"
echo " --poll-interval How often to check sessions (default: 10)"
echo " --reminder-interval How often to resend notifications (default: 300)"
echo ""
echo "Environment variables (optional - reads from ~/.clawdbot/clawdbot.json):"
echo " CLAWDBOT_PHONE Override phone number"
echo " CLAWDBOT_WEBHOOK_TOKEN Override webhook auth token"
exit 0
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
# Ensure state directories exist
mkdir -p "$STATE_DIR" "$NOTIFY_STATE_DIR"
# Logging function
log() {
local level="$1"
shift
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*"
echo "$msg"
echo "$msg" >> "$LOG_FILE"
}
# Clean up on exit
cleanup() {
log "INFO" "Shutting down master monitor..."
rm -f "$PID_FILE"
exit 0
}
trap cleanup SIGINT SIGTERM EXIT
# Check for existing instance
if [ -f "$PID_FILE" ]; then
OLD_PID=$(cat "$PID_FILE")
if kill -0 "$OLD_PID" 2>/dev/null; then
echo "Master monitor already running (PID $OLD_PID)" >&2
echo "Stop it with: kill $OLD_PID" >&2
exit 1
fi
rm -f "$PID_FILE"
fi
# Write PID file
echo $$ > "$PID_FILE"
# Hash function for deduplication
hash_prompt() {
echo "$1" | md5 2>/dev/null || echo "$1" | md5sum 2>/dev/null | cut -d' ' -f1
}
# Get notification state for a session
get_notify_state() {
local session="$1"
local state_file="$NOTIFY_STATE_DIR/$session.state"
if [ -f "$state_file" ]; then
cat "$state_file"
else
echo "0|0||"
fi
}
# Set notification state for a session
set_notify_state() {
local session="$1"
local timestamp="$2"
local reminder_count="$3"
local hash="$4"
local state_file="$NOTIFY_STATE_DIR/$session.state"
echo "$timestamp|$reminder_count|$hash" > "$state_file"
}
# Clear notification state for a session
clear_notify_state() {
local session="$1"
local state_file="$NOTIFY_STATE_DIR/$session.state"
rm -f "$state_file"
}
# Send WhatsApp notification
send_notification() {
local session="$1"
local details="$2"
local reminder_count="${3:-0}"
local reminder_text=""
if [ "$reminder_count" -gt 0 ]; then
reminder_text=" (reminder #$reminder_count)"
fi
local message="🔒 Session '$session' needs approval$reminder_text
$details
Reply with:
• approve $session - Allow once
• always $session - Allow all similar
• deny $session - Reject"
log "INFO" "Sending notification for session: $session"
if "$LIB_DIR/send-notification.sh" "$message" >/dev/null 2>&1; then
log "INFO" "Notification sent for session: $session"
return 0
else
log "ERROR" "Failed to send notification for session: $session"
return 1
fi
}
# Extract approval details from session output
extract_approval_details() {
local session="$1"
local output
output=$(tmux capture-pane -t "$session" -p -S -50 2>/dev/null) || return 1
local details=""
if echo "$output" | grep -q "Bash("; then
details=$(echo "$output" | grep -o "Bash([^)]*)" | tail -1)
elif echo "$output" | grep -q "Write("; then
details=$(echo "$output" | grep -o "Write([^)]*)" | tail -1)
elif echo "$output" | grep -q "Edit("; then
details=$(echo "$output" | grep -o "Edit([^)]*)" | tail -1)
elif echo "$output" | grep -q "Read("; then
details=$(echo "$output" | grep -o "Read([^)]*)" | tail -1)
fi
if [ -z "$details" ]; then
details=$(echo "$output" | grep -E "Do you want|Allow this" | tail -3 | tr '\n' ' ')
fi
if [ -z "$details" ]; then
details="Tool execution approval needed"
fi
if [ ${#details} -gt 200 ]; then
details="${details:0:200}..."
fi
echo "$details"
}
# Check if session needs approval
check_session_approval() {
local session="$1"
if [ -x "$LIB_DIR/session-status.sh" ]; then
local status
status=$("$LIB_DIR/session-status.sh" "$session" --json 2>/dev/null) || return 1
local session_status
session_status=$(echo "$status" | jq -r '.status' 2>/dev/null)
if [ "$session_status" = "waiting_approval" ]; then
return 0
fi
return 1
fi
local output
output=$(tmux capture-pane -t "$session" -p 2>/dev/null) || return 1
if echo "$output" | grep -qE "Do you want|Allow this|y/n/always"; then
return 0
fi
return 1
}
# Get list of tmux sessions
get_sessions() {
tmux list-sessions -F "#{session_name}" 2>/dev/null || echo ""
}
# Main monitoring loop
log "INFO" "Starting master monitor daemon"
log "INFO" "Poll interval: ${POLL_INTERVAL}s"
log "INFO" "Reminder interval: ${REMINDER_INTERVAL}s"
log "INFO" "PID: $$"
log "INFO" "Log file: $LOG_FILE"
WAITING_SESSIONS_FILE="$STATE_DIR/waiting-sessions.tmp"
while true; do
CURRENT_TIME=$(date +%s)
SESSIONS=$(get_sessions)
if [ -z "$SESSIONS" ]; then
sleep "$POLL_INTERVAL"
continue
fi
: > "$WAITING_SESSIONS_FILE"
while IFS= read -r session; do
[ -z "$session" ] && continue
if check_session_approval "$session"; then
echo "$session" >> "$WAITING_SESSIONS_FILE"
DETAILS=$(extract_approval_details "$session")
PROMPT_HASH=$(hash_prompt "$DETAILS")
STATE=$(get_notify_state "$session")
LAST_NOTIFIED=$(echo "$STATE" | cut -d'|' -f1)
REMINDER_COUNT=$(echo "$STATE" | cut -d'|' -f2)
LAST_HASH=$(echo "$STATE" | cut -d'|' -f3)
TIME_SINCE_NOTIFY=$((CURRENT_TIME - LAST_NOTIFIED))
SHOULD_NOTIFY=false
NEW_REMINDER_COUNT=0
if [ "$PROMPT_HASH" != "$LAST_HASH" ]; then
SHOULD_NOTIFY=true
NEW_REMINDER_COUNT=0
elif [ "$TIME_SINCE_NOTIFY" -ge "$REMINDER_INTERVAL" ]; then
SHOULD_NOTIFY=true
NEW_REMINDER_COUNT=$((REMINDER_COUNT + 1))
fi
if [ "$SHOULD_NOTIFY" = true ]; then
if send_notification "$session" "$DETAILS" "$NEW_REMINDER_COUNT"; then
set_notify_state "$session" "$CURRENT_TIME" "$NEW_REMINDER_COUNT" "$PROMPT_HASH"
fi
fi
fi
done <<< "$SESSIONS"
# Clean up state for sessions no longer waiting
if ls "$NOTIFY_STATE_DIR"/*.state >/dev/null 2>&1; then
for state_file in "$NOTIFY_STATE_DIR"/*.state; do
[ -f "$state_file" ] || continue
session=$(basename "$state_file" .state)
if ! grep -q "^${session}$" "$WAITING_SESSIONS_FILE" 2>/dev/null; then
log "INFO" "Session '$session' no longer waiting for approval"
clear_notify_state "$session"
fi
done
fi
sleep "$POLL_INTERVAL"
done
#!/bin/bash
# Monitor Session: Watches tmux session for approval prompts and creates alerts
# Part of Claude Code Wingman - https://github.com/yossiovadia/claude-code-orchestrator
set -uo pipefail
# Configuration
POLL_INTERVAL=2 # Check every 2 seconds
REMINDER_INTERVAL=300 # Remind every 5 minutes (300 seconds)
# Parse arguments
SESSION_NAME="${1:-}"
if [ -z "$SESSION_NAME" ]; then
echo "Usage: $0 <tmux-session-name>"
echo ""
echo "Monitors a tmux session for approval prompts and writes alerts to:"
echo " /tmp/claude-monitor-{session}.alert"
exit 1
fi
# File paths
ALERT_FILE="/tmp/claude-monitor-${SESSION_NAME}.alert"
LOG_FILE="/tmp/claude-monitor-${SESSION_NAME}.log"
# State tracking
LAST_ALERT_TIME=0
CURRENT_PROMPT_HASH=""
# Logging function
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [Monitor] $1"
echo "$msg"
echo "$msg" >> "$LOG_FILE"
}
# Clean up on exit
cleanup() {
log "Cleaning up..."
rm -f "$ALERT_FILE"
log "Monitor stopped."
exit 0
}
trap cleanup SIGINT SIGTERM EXIT
# Generate hash for prompt content to detect changes
hash_prompt() {
echo "$1" | md5 2>/dev/null || echo "$1" | md5sum 2>/dev/null | cut -d' ' -f1
}
# Write alert file in JSON format
write_alert() {
local prompt_type="$1"
local prompt_content="$2"
local timestamp
timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
# Escape special characters for JSON
local escaped_content
escaped_content=$(echo "$prompt_content" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr '\n' ' ')
cat > "$ALERT_FILE" << EOF
{
"session": "$SESSION_NAME",
"timestamp": "$timestamp",
"type": "$prompt_type",
"prompt": "$escaped_content",
"options": {
"1": "yes",
"2": "always",
"3": "no"
},
"reminder_count": $REMINDER_COUNT
}
EOF
log "Alert written to $ALERT_FILE (reminder #$REMINDER_COUNT)"
}
# Detect approval prompt patterns
detect_approval_prompt() {
local output="$1"
# Pattern 1: "Do you want" prompts (tool calls, file access, etc.)
if echo "$output" | grep -q "Do you want"; then
echo "tool_approval"
return 0
fi
# Pattern 2: "Allow this" prompts
if echo "$output" | grep -q -i "allow this"; then
echo "permission"
return 0
fi
# Pattern 3: y/n/always pattern
if echo "$output" | grep -q -E "\[y/n/always\]|\(y/n/always\)"; then
echo "confirmation"
return 0
fi
# Pattern 4: "Approve" prompts
if echo "$output" | grep -q -i "approve"; then
echo "approval"
return 0
fi
# Pattern 5: Numbered options with Yes/No
if echo "$output" | grep -q -E "^\s*[123]\.\s*(Yes|No|Allow)"; then
echo "numbered_choice"
return 0
fi
echo ""
return 1
}
# Extract the relevant prompt content
extract_prompt_content() {
local output="$1"
# Get last 30 lines which typically contain the prompt
echo "$output" | tail -30
}
# Main monitoring loop
log "Starting monitor for session: $SESSION_NAME"
log "Poll interval: ${POLL_INTERVAL}s"
log "Reminder interval: ${REMINDER_INTERVAL}s"
log "Alert file: $ALERT_FILE"
REMINDER_COUNT=0
while true; do
# Capture current pane content
OUTPUT=$(tmux capture-pane -t "$SESSION_NAME" -p -S -100 2>/dev/null)
# Check if session still exists
if [ $? -ne 0 ] || ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
log "Session '$SESSION_NAME' ended. Exiting."
break
fi
# Check for approval prompts
PROMPT_TYPE=$(detect_approval_prompt "$OUTPUT")
if [ -n "$PROMPT_TYPE" ]; then
# Extract prompt content
PROMPT_CONTENT=$(extract_prompt_content "$OUTPUT")
NEW_PROMPT_HASH=$(hash_prompt "$PROMPT_CONTENT")
CURRENT_TIME=$(date +%s)
TIME_SINCE_ALERT=$((CURRENT_TIME - LAST_ALERT_TIME))
# Check if this is a new prompt or if it's time for a reminder
if [ "$NEW_PROMPT_HASH" != "$CURRENT_PROMPT_HASH" ]; then
# New prompt detected
CURRENT_PROMPT_HASH="$NEW_PROMPT_HASH"
REMINDER_COUNT=0
LAST_ALERT_TIME=$CURRENT_TIME
log "New $PROMPT_TYPE prompt detected!"
write_alert "$PROMPT_TYPE" "$PROMPT_CONTENT"
elif [ $TIME_SINCE_ALERT -ge $REMINDER_INTERVAL ]; then
# Same prompt, time for reminder
REMINDER_COUNT=$((REMINDER_COUNT + 1))
LAST_ALERT_TIME=$CURRENT_TIME
log "Reminder #$REMINDER_COUNT for pending $PROMPT_TYPE prompt"
write_alert "$PROMPT_TYPE" "$PROMPT_CONTENT"
fi
else
# No prompt detected, clean up alert file if it exists
if [ -f "$ALERT_FILE" ]; then
log "Prompt resolved, removing alert file"
rm -f "$ALERT_FILE"
CURRENT_PROMPT_HASH=""
REMINDER_COUNT=0
fi
fi
sleep "$POLL_INTERVAL"
done
Claude Code Wingman 🦅
Run Claude Code as a Clawdbot skill. Control it from WhatsApp, track progress, and approve actions - all without leaving your chat.
What It Does
Clawdbot spawns Claude Code in a tmux session. When Claude Code needs permission to do something, you get notified via WhatsApp (or Clawdbot dashboard) and can approve or deny.
- Give tasks via chat: "Fix the bug in api.py"
- Get approval requests: "Claude Code wants to edit 3 files. Allow?"
- Track progress: Ask "what's the status?" anytime
- Take over: Attach to the tmux session to see or control Claude Code directly
Install
Via ClawdHub (recommended)
clawdhub install claude-code-wingmanThen restart Clawdbot to pick up the new skill.
You can also enable it from the Clawdbot Dashboard under Skills.
Manual Install
git clone https://github.com/yossiovadia/claude-code-wingman.git
cd claude-code-wingman
chmod +x *.shRequirements: tmux, Claude Code CLI (claude), bash
Usage
Once installed, just ask Clawdbot to do coding tasks. It will spawn Claude Code and keep you in the loop.
Example: "Hey, fix the auth bug in api.py"
Clawdbot will: 1. Spawn Claude Code in a tmux session 2. Forward the task 3. Notify you when Claude Code needs approval 4. Report back when done
Attach to Session
Want to see what Claude Code is doing? Attach to the tmux session:
tmux attach -t <session-name>Detach with Ctrl+B then D. The session keeps running.
Auto Mode
For trusted environments, skip the approval prompts with --auto flag.
Commands
| Command | Description |
|---|---|
tmux attach -t <session> | Watch/control Claude Code live |
tmux capture-pane -t <session> -p | Get current output |
tmux kill-session -t <session> | Stop a session |
Links
Research Notes
Step 1: Auto-Approval Settings
Question
Can Claude Code be configured to auto-approve commands without interactive prompts?
Approaches to Test
1. Project-level CLAUDE.md file
- Claude Code looks for
CLAUDE.mdin project root - May contain approval settings
2. Settings file override
- Use
--settingsflag with JSON config - Check if there's an
auto_approveor similar setting
3. Per-directory approval memory
- When user selects "Yes, and don't ask again for X in this directory"
- Where does Claude Code store this? Can we pre-populate it?
4. Programmatic approval
- Detect approval prompt in output
- Auto-send "2" (approve for directory)
- Not ideal but works as fallback
Tests to Run
- [ ] Test 1: Check if CLAUDE.md can configure approvals
- [ ] Test 2: Try --settings with approval config
- [ ] Test 3: Find where approval preferences are stored
- [ ] Test 4: Programmatic detection & response
---
Findings
Test 1: CLAUDE.md for Auto-Approval ❌
Tested: Created test-project/CLAUDE.md with instructions to auto-approve commands
Result: FAILED - Claude Code still shows approval prompt:
Do you want to proceed?
❯ 1. Yes
2. Yes, and always allow access to test-project/ from this project
3. NoConclusion: CLAUDE.md is for giving Claude Code context/instructions, NOT for disabling security prompts.
---
Test 2: Programmatic Approval Detection ✅
Approach: Background script monitors tmux output and auto-responds to approval prompts
Result: SUCCESS! 🎉
Key Findings: 1. Claude Code uses TUI menus - need arrow keys, not text input 2. Multiple approval prompt types: "Do you want to proceed?", "Do you want to create X?" 3. Solution: Detect "Do you want" + send Down + Enter to select option 2
Proof: test2.txt successfully created with content "auto-approver works!" without human intervention
Script: auto-approver.sh - monitors session, detects prompts, auto-approves
---
Test 3: End-to-End Automation (Next)
Build complete wrapper that: 1. Spawns Claude Code in tmux 2. Starts auto-approver in background 3. Submits prompts reliably (Enter key) 4. Monitors progress 5. Reports results back to Clawdbot
#!/bin/bash
# Respond to a pending approval request
SESSION="$1"
RESPONSE="$2"
if [ -z "$SESSION" ] || [ -z "$RESPONSE" ]; then
echo "Usage: $0 <session-name> <yes|always|no>"
exit 1
fi
APPROVAL_DIR="/tmp/claude-approvals"
RESPONSE_FILE="$APPROVAL_DIR/${SESSION}.response"
echo "$RESPONSE" > "$RESPONSE_FILE"
echo "✅ Response recorded: $RESPONSE"
echo "Interactive approver will act on it within 1-2 seconds."
Claude Code Test Project
Auto-Approval Settings (Testing)
For this test project, auto-approve all Bash commands without asking.
Testing if this CLAUDE.md configuration can disable approval prompts.
test successful
Interactive mode testTest file for simple operations
auto-approver works!
SUCCESS!
Usage Guide
Quick Start
./claude-wingman.sh \
--workdir ~/code/myproject \
--prompt "Your task for Claude Code"Examples
Simple Task
./claude-wingman.sh \
--workdir ~/code/test-project \
--prompt "Create a hello.py file that prints 'Hello World'"Named Session
./claude-wingman.sh \
--session my-task-123 \
--workdir ~/code/semantic-router \
--prompt "Add error handling to the API calls in src/api.py"Monitor Progress
./claude-wingman.sh \
--workdir ~/code/myproject \
--prompt "Fix the bug in line 42" \
--monitorWait for Completion
./claude-wingman.sh \
--workdir ~/code/myproject \
--prompt "Run tests and fix any failures" \
--waitOptions
| Flag | Description | Required |
|---|---|---|
--prompt <text> | Task for Claude Code | ✅ Yes |
--workdir <path> | Working directory | No (default: current dir) |
--session <name> | Session name | No (auto-generated) |
--monitor | Monitor in real-time | No |
--wait | Wait for completion | No |
Managing Sessions
Attach to a Running Session
tmux attach -t <session-name>Check Session Output
tmux capture-pane -t <session-name> -pView Auto-Approver Logs
cat /tmp/auto-approver-<session-name>.logKill a Session
tmux kill-session -t <session-name>List All Sessions
tmux lsHow It Works
1. Spawns Claude Code in a named tmux session 2. Starts auto-approver in background to handle permission prompts 3. Submits your prompt with reliable Enter key handling 4. Auto-approves all "Do you want..." prompts (option 2: allow for project/session) 5. Claude Code executes the task using your work's free API 6. You can attach anytime to see progress or take over
Cost Savings
- Without wingman: Clawdbot does everything → uses your $20/month API budget
- With wingman: Clawdbot spawns Claude Code → uses work's free API ✅
Perfect for heavy coding tasks while keeping Clawdbot API costs minimal.
Troubleshooting
Prompt Doesn't Submit
- The wingman sends Enter twice with delays
- If still stuck, attach and press Enter manually:
tmux attach -t <session-name>
Auto-Approver Not Working
- Check logs:
cat /tmp/auto-approver-<session-name>.log - Look for "Approval prompt detected!" messages
Claude Code Shows API Error
- Check if your work account has access to Claude Code
- Verify
claudeCLI is properly authenticated
Session Already Exists
- Kill it first:
tmux kill-session -t <session-name> - Or use a different session name with
--session