Skip to content

Instantly share code, notes, and snippets.

@thanakijwanavit
Last active March 1, 2026 23:51
Show Gist options
  • Select an option

  • Save thanakijwanavit/c0877d834e288de104b38f3f8cda233c to your computer and use it in GitHub Desktop.

Select an option

Save thanakijwanavit/c0877d834e288de104b38f3f8cda233c to your computer and use it in GitHub Desktop.
Gas Town — Claude Code Tools (ais CLI, credit checker, orchestrator skill, tmux guide)

Agent Setup Guide

How to configure Claude Code, Kimi Code, and OpenClaw instances for orchestrated multi-agent work.


1. Claude Code Sub-Agents

Install Plugins

Each Claude Code account should have these plugins installed. Run inside a Claude Code session (or for each account):

# Essential for autonomous agents
/plugin install superpowers@claude-plugins-official
/plugin install code-review@claude-plugins-official
/plugin install code-simplifier@claude-plugins-official

# Optional but useful
/plugin install playwright@claude-plugins-official       # browser automation
/plugin install typescript-lsp@claude-plugins-official    # TS language server
/plugin install claude-code-setup@claude-plugins-official # setup automation

To install across all accounts:

for acct in cc1 cc2 cc3 hataricc nicxxx; do
  echo "=== Setting up $acct ==="
  CLAUDE_CONFIG_DIR=~/.claude-accounts/$acct claude --dangerously-skip-permissions \
    -c "/plugin install superpowers@claude-plugins-official" 2>/dev/null || true
done

Settings for Autonomous Operation

Each account's ~/.claude-accounts/<name>/settings.json should allow unattended execution:

{
  "permissions": {
    "allow": [
      "Bash(*)",
      "Read(*)",
      "Write(*)",
      "Edit(*)",
      "Glob(*)",
      "Grep(*)",
      "WebSearch",
      "WebFetch(*)"
    ]
  }
}

Or simply use --dangerously-skip-permissions flag (which ais create --yolo does automatically for Claude).

CLAUDE.md Template for Sub-Agents

Drop this in the working directory before spawning the agent:

# Sub-Agent Standing Orders

You are a sub-agent managed by an orchestrator. Follow these rules:

1. **Stay focused** — complete only the task you were given
2. **Stay in scope** — only modify files in this directory
3. **Report completion** — when done, print: TASK COMPLETE: <summary>
4. **Report errors** — if stuck, print: TASK BLOCKED: <reason>
5. **No git push** — commit locally but do NOT push
6. **No interactive prompts** — never ask for confirmation, just proceed

MCP Servers (Optional, Advanced)

For agents that need to control other tmux sessions or manage processes:

{
  "mcpServers": {
    "tmux": {
      "command": "npx",
      "args": ["-y", "@nickgnd/tmux-mcp"]
    }
  }
}

Add to ~/.claude-accounts/<name>/settings.json under mcpServers.

Claude Code Account Reference

Account Tier Use Case
cc1 Sonnet Fast parallel tasks, simple fixes
cc2 Sonnet Fast parallel tasks, simple fixes
cc3 Opus Complex reasoning, architecture
hataricc Opus Complex tasks, fallback
nicxxx Opus Complex tasks, fallback

2. Kimi Code Sub-Agents

Account Setup

API Key method (recommended for agents):

kimi-account add sk-kimi-YOUR_KEY_HERE

Get keys from platform.kimi.com. API keys never expire.

OAuth method (for interactive use):

kimi-account login              # auto-number
kimi-account login 3            # specific account

Autonomous Operation

Use --yolo flag for auto-approve mode. ais create --yolo handles this automatically.

# Manual
KIMI_SHARE_DIR=~/.kimi-accounts/1 kimi --yolo

# Via ais (recommended)
ais create my-agent -a kimi -A 1 --yolo -c "your task"

Kimi Skills

Kimi has its own skills directory at ~/.kimi/skills/. To add a skill:

mkdir -p ~/.kimi/skills/my-skill/
cat > ~/.kimi/skills/my-skill/SKILL.md << 'EOF'
---
name: my-skill
description: Description of the skill
---

# Skill content here
EOF

Kimi vs Claude — When to Use Which

Factor Claude Code Kimi Code
Cost Credits (limited) Free
Rate limits Per-account credits Request-based
Reasoning Opus is strongest K2.5 is good, not Opus-level
Speed Fast (Sonnet), slow (Opus) Fast
Best for Complex multi-file refactors, debugging Test fixing, boilerplate, error handling
Auto-approve --dangerously-skip-permissions --yolo
Account env var CLAUDE_CONFIG_DIR KIMI_SHARE_DIR

Strategy: Use Kimi for high-volume simple tasks (saves Claude credits). Use Claude Opus for tasks requiring deep reasoning. Use Claude Sonnet for medium-complexity tasks.

Kimi Account Reference

Run kimi-account list to see all accounts.

Account Auth Expiry
1 OAuth 30-day refresh token
2 API key Never
3+ Add more kimi-account add <key>

3. OpenClaw Orchestrator

The orchestrator is an OpenClaw agent that manages all the sub-agents.

Install the Orchestrator Skill

mkdir -p ~/.openclaw/skills/ais-orchestrator/scripts/
cp /path/to/orchestrator-skill.md ~/.openclaw/skills/ais-orchestrator/SKILL.md
cp ~/.local/bin/ais ~/.openclaw/skills/ais-orchestrator/scripts/ais
chmod +x ~/.openclaw/skills/ais-orchestrator/scripts/ais

Required Built-in Skills

These ship with OpenClaw and should already be available:

Skill Purpose
tmux Raw tmux send-keys / capture-pane control
coding-agent Spawn Claude/Codex/OpenCode as background processes
clawhub Discover and install skills from ClawHub registry
github GitHub repo management
gh-issues GitHub issue tracking

Verify they're available:

ls ~/.local/lib/node_modules/openclaw/skills/ | grep -E "tmux|coding-agent|clawhub|github"

OpenClaw Configuration

The orchestrator's ~/.openclaw/openclaw.json should have:

{
  "agents": {
    "defaults": {
      "model": {
        "primary": "openrouter/moonshotai/kimi-k2.5",
        "fallbacks": ["openrouter/qwen/qwen3-coder:free"]
      }
    }
  }
}

This means the orchestrator itself runs on Kimi K2.5 (free) — it doesn't consume Claude credits for management overhead.

Optional: Additional ClawHub Skills

Browse and install from ClawHub if needed:

# Search for relevant skills
clawhub search "task management"
clawhub search "monitoring"

# Install a skill
clawhub install <skill-slug>

# Update all installed skills
clawhub update --all

Security warning: ClawHub is an open registry. Vet skills before installing — check the source code, author reputation, and avoid skills that request elevated permissions unnecessarily.

Optional: MCP Servers for the Orchestrator

If using Claude Code as the orchestrator instead of OpenClaw:

{
  "mcpServers": {
    "tmux": {
      "command": "npx",
      "args": ["-y", "@nickgnd/tmux-mcp"]
    },
    "taskqueue": {
      "command": "npx",
      "args": ["-y", "taskqueue-mcp"]
    }
  }
}
  • tmux-mcp (nickgnd/tmux-mcp) — lets Claude Code directly read/control tmux sessions
  • taskqueue-mcp (chriscarrollsmith/taskqueue-mcp) — structured task queue with approval checkpoints
  • pm-mcp (patrickjm/pm-mcp) — process manager for background tasks, log searching

4. ais CLI Setup

The ais tool is the glue between the orchestrator and sub-agents.

Install

curl -sL <gist-raw-url>/ais.sh -o ~/.local/bin/ais
chmod +x ~/.local/bin/ais

Or copy from this gist's ais.sh file.

Verify

ais --version          # should print "ais 1.0.0"
ais accounts           # should list Claude + Kimi accounts
ais ls                 # should show no sessions (or existing ones)

Quick Test

# Create a test session
ais create test -a kimi -A 1 -c "say hello"
sleep 12

# Check it's running
ais ls
ais inspect test -n 10

# Clean up
ais kill test

5. Full Stack Verification

After setting up everything, verify the full orchestration chain:

# 1. Check accounts are configured
ais accounts

# 2. Check Claude accounts have credit
bash check-credit.sh

# 3. Check Kimi accounts are working
kimi-account check

# 4. Test spawning agents
ais create test-claude -a claude -A cc1 --yolo -c "echo hello from claude"
ais create test-kimi -a kimi -A 1 --yolo -c "say hello from kimi"
sleep 15

# 5. Verify both running
ais ls

# 6. Check output
ais inspect test-claude -n 10
ais inspect test-kimi -n 10

# 7. Clean up
ais kill --all

# 8. Check OpenClaw skill is available
ls ~/.openclaw/skills/ais-orchestrator/

Marketplace Reference

Claude Code Plugins

Source URL
Official github.com/anthropics/claude-plugins-official
In-app /plugin > Discover inside Claude Code
Community claudecodeplugins.io

OpenClaw Skills

Source URL
ClawHub clawhub.ai
Built-in ~/.local/lib/node_modules/openclaw/skills/
Custom ~/.openclaw/skills/
Curated github.com/VoltAgent/awesome-openclaw-skills

MCP Servers

Source URL
Official registry registry.modelcontextprotocol.io
Community (17K+) mcp.so
Curated list github.com/punkpeye/awesome-mcp-servers
Official implementations github.com/modelcontextprotocol/servers
#!/bin/bash
# ais — AI coding agent session manager
#
# Create, monitor, and control Claude Code / Kimi Code sessions in tmux.
#
# Usage:
# ais create <name> -a claude|kimi -A <account> [-c "cmd"] [--yolo]
# ais ls List all managed sessions
# ais inspect <name> [-n lines] Capture current output
# ais inject <name> "text" Send text into session
# ais watch <name> [-i secs] Live-monitor session
# ais logs <name> [-o file] Save scrollback to file
# ais kill <name|--all> Graceful shutdown
# ais accounts List available accounts
set -uo pipefail
# ═══════════════════════════════════════════════════════════════════════
# Constants
# ═══════════════════════════════════════════════════════════════════════
VERSION="1.0.0"
CLAUDE_BIN=~/.local/bin/claude
KIMI_BIN=~/.local/bin/kimi
CLAUDE_ACCOUNTS_DIR=~/.claude-accounts
KIMI_ACCOUNTS_DIR=~/.kimi-accounts
DEFAULT_WIDTH=160
DEFAULT_HEIGHT=50
CLAUDE_LOAD_TIME=14
KIMI_LOAD_TIME=8
RATE_LIMIT_PATTERN='rate.?limit|429|overloaded|quota.?exceeded|too many requests|credit balance is too low|insufficient_quota|hit your limit'
# ═══════════════════════════════════════════════════════════════════════
# Utilities
# ═══════════════════════════════════════════════════════════════════════
die() { echo "ais: error: $*" >&2; exit 1; }
warn() { echo "ais: warning: $*" >&2; }
info() { echo "ais: $*"; }
sanitize_utf8() {
LC_ALL=C sed 's/[^[:print:][:space:]]//g' | iconv -f utf-8 -t utf-8 -c 2>/dev/null || cat
}
session_exists() {
tmux has-session -t "$1" 2>/dev/null
}
is_managed() {
local val
val=$(tmux show-environment -t "$1" AIS_MANAGED 2>/dev/null)
[[ "$val" == "AIS_MANAGED=1" ]]
}
get_meta() {
local session="$1" key="$2"
local val
val=$(tmux show-environment -t "$session" "$key" 2>/dev/null)
echo "${val#*=}"
}
require_session() {
local name="$1"
session_exists "$name" || die "session '$name' not found (run 'ais ls' to see sessions)"
}
validate_name() {
local name="$1"
[[ "$name" =~ ^[a-zA-Z0-9_-]+$ ]] || die "invalid session name: '$name' (use letters, numbers, hyphens, underscores)"
}
# ═══════════════════════════════════════════════════════════════════════
# usage
# ═══════════════════════════════════════════════════════════════════════
usage() {
cat << 'EOF'
ais — AI coding agent session manager
Usage:
ais create <name> [options] Create a new agent session
ais ls List all managed sessions
ais inspect <name> [options] Capture current output
ais inject <name> <text> Send text into a session
ais watch <name> [options] Live-monitor a session
ais logs <name> [options] Save scrollback to file
ais kill <name|--all> [options] Shut down a session
ais accounts List available accounts
Create options:
-a, --agent TYPE Agent: claude, kimi (default: kimi)
-A, --account ID Account: cc1..nicxxx for claude, 1..N for kimi
-c, --cmd TEXT Command to inject after agent loads
-d, --dir PATH Working directory (default: current)
--yolo Auto-approve mode
--attach Attach to session after creation
--size WxH Terminal size (default: 160x50)
-- Pass remaining flags to agent CLI
Inspect options:
-n, --lines N Lines to capture (default: 50)
--rate-limit Check for rate limit patterns
Inject options:
--no-enter Send text without pressing Enter
Watch options:
-n, --lines N Lines per refresh (default: 30)
-i, --interval SECS Refresh interval (default: 2)
--until PATTERN Exit when pattern appears
Kill options:
--all Kill all managed sessions
--force Skip graceful shutdown
--save Save scrollback before killing
Examples:
ais create worker1 -a claude -A cc1 -c "fix the auth bug"
ais create kimi-task -a kimi -A 2 --yolo
ais inspect worker1 -n 200
ais inject worker1 "run the tests"
ais watch worker1 -i 5
ais kill worker1
ais kill --all
EOF
}
# ═══════════════════════════════════════════════════════════════════════
# cmd_create
# ═══════════════════════════════════════════════════════════════════════
cmd_create() {
local name="" agent="kimi" account="" cmd="" dir="" yolo=false attach=false
local width=$DEFAULT_WIDTH height=$DEFAULT_HEIGHT
local extra_args=()
local parsing_extra=false
while [[ $# -gt 0 ]]; do
if [[ "$parsing_extra" == true ]]; then
extra_args+=("$1"); shift; continue
fi
case "$1" in
-a|--agent) agent="$2"; shift 2 ;;
-A|--account) account="$2"; shift 2 ;;
-c|--cmd) cmd="$2"; shift 2 ;;
-d|--dir) dir="$2"; shift 2 ;;
--yolo) yolo=true; shift ;;
--attach) attach=true; shift ;;
--size) width="${2%%x*}"; height="${2##*x}"; shift 2 ;;
--) parsing_extra=true; shift ;;
-*) die "unknown option: $1" ;;
*)
if [[ -z "$name" ]]; then
name="$1"; shift
else
die "unexpected argument: $1"
fi
;;
esac
done
[[ -z "$name" ]] && die "session name required (ais create <name> ...)"
validate_name "$name"
session_exists "$name" && die "session '$name' already exists (use 'ais kill $name' first)"
# Resolve agent binary and account env var
local env_var="" config_dir="" binary="" load_time=0
case "$agent" in
claude)
binary="$CLAUDE_BIN"
env_var="CLAUDE_CONFIG_DIR"
[[ -z "$account" ]] && die "account required for claude (-A cc1, cc2, cc3, hataricc, nicxxx)"
config_dir="$CLAUDE_ACCOUNTS_DIR/$account"
[[ -d "$config_dir" ]] || die "claude account '$account' not found at $config_dir"
load_time=$CLAUDE_LOAD_TIME
[[ "$yolo" == true ]] && extra_args=("--dangerously-skip-permissions" "${extra_args[@]}")
;;
kimi)
binary="$KIMI_BIN"
env_var="KIMI_SHARE_DIR"
[[ -z "$account" ]] && die "account required for kimi (-A 1, 2, ...)"
config_dir="$KIMI_ACCOUNTS_DIR/$account"
[[ -d "$config_dir" ]] || die "kimi account '$account' not found at $config_dir"
load_time=$KIMI_LOAD_TIME
[[ "$yolo" == true ]] && extra_args=("--yolo" "${extra_args[@]}")
;;
*)
die "unknown agent: $agent (use 'claude' or 'kimi')"
;;
esac
[[ ! -x "$binary" ]] && [[ ! -f "$binary" ]] && die "agent binary not found: $binary"
# Build launch command
local launch_cmd="${env_var}='${config_dir}' '${binary}'"
for arg in "${extra_args[@]}"; do
launch_cmd+=" '$arg'"
done
# Working directory
local tmux_dir_args=()
if [[ -n "$dir" ]]; then
[[ -d "$dir" ]] || die "directory not found: $dir"
tmux_dir_args=(-c "$dir")
fi
# Create tmux session
tmux new-session -d -s "$name" -x "$width" -y "$height" "${tmux_dir_args[@]}"
# Set metadata
tmux set-environment -t "$name" AIS_MANAGED 1
tmux set-environment -t "$name" AIS_AGENT "$agent"
tmux set-environment -t "$name" AIS_ACCOUNT "$account"
tmux set-environment -t "$name" AIS_DIR "${dir:-$(pwd)}"
tmux set-environment -t "$name" AIS_CREATED "$(date +%s)"
# Launch agent
tmux send-keys -t "$name" "$launch_cmd" Enter
info "created '$name' ($agent / account $account)"
# Schedule command injection after load time
if [[ -n "$cmd" ]]; then
info "command will be injected in ~${load_time}s: $cmd"
(
sleep "$load_time"
if tmux has-session -t "$name" 2>/dev/null; then
tmux send-keys -t "$name" -l -- "$cmd"
sleep 0.2
tmux send-keys -t "$name" Enter
fi
) &
disown
fi
info "attach: tmux attach -t $name"
if [[ "$attach" == true ]]; then
exec tmux attach-session -t "$name"
fi
}
# ═══════════════════════════════════════════════════════════════════════
# cmd_ls
# ═══════════════════════════════════════════════════════════════════════
cmd_ls() {
local show_all=false
while [[ $# -gt 0 ]]; do
case "$1" in
-a|--all) show_all=true; shift ;;
*) die "unknown option: $1" ;;
esac
done
local sessions
sessions=$(tmux list-sessions -F '#{session_name}' 2>/dev/null) || true
if [[ -z "$sessions" ]]; then
info "no tmux sessions running"
return
fi
printf " %-20s %-8s %-10s %-30s %s\n" "NAME" "AGENT" "ACCOUNT" "DIR" "AGE"
printf " %-20s %-8s %-10s %-30s %s\n" "────────────────────" "────────" "──────────" "──────────────────────────────" "────────"
local found=false
while IFS= read -r sess; do
local managed
managed=$(tmux show-environment -t "$sess" AIS_MANAGED 2>/dev/null || true)
if [[ "$managed" == "AIS_MANAGED=1" ]]; then
found=true
local agent account dir created age_str
agent=$(get_meta "$sess" AIS_AGENT)
account=$(get_meta "$sess" AIS_ACCOUNT)
dir=$(get_meta "$sess" AIS_DIR)
created=$(get_meta "$sess" AIS_CREATED)
# Calculate age
age_str="?"
if [[ -n "$created" ]] && [[ "$created" =~ ^[0-9]+$ ]]; then
local now elapsed
now=$(date +%s)
elapsed=$((now - created))
if [[ $elapsed -lt 60 ]]; then
age_str="${elapsed}s"
elif [[ $elapsed -lt 3600 ]]; then
age_str="$((elapsed / 60))m"
else
age_str="$((elapsed / 3600))h$((elapsed % 3600 / 60))m"
fi
fi
# Shorten dir for display
local short_dir="${dir/#$HOME/\~}"
[[ ${#short_dir} -gt 30 ]] && short_dir="...${short_dir: -27}"
printf " %-20s %-8s %-10s %-30s %s\n" "$sess" "$agent" "$account" "$short_dir" "$age_str"
elif [[ "$show_all" == true ]]; then
printf " %-20s %-8s %-10s %-30s %s\n" "$sess" "-" "-" "-" "-"
fi
done <<< "$sessions"
if [[ "$found" == false ]] && [[ "$show_all" == false ]]; then
info "no managed sessions (use 'ais ls -a' to show all tmux sessions)"
fi
}
# ═══════════════════════════════════════════════════════════════════════
# cmd_inspect
# ═══════════════════════════════════════════════════════════════════════
cmd_inspect() {
local name="" lines=50 check_rate_limit=false
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--lines) lines="$2"; shift 2 ;;
--rate-limit) check_rate_limit=true; shift ;;
-*) die "unknown option: $1" ;;
*)
if [[ -z "$name" ]]; then
name="$1"; shift
else
die "unexpected argument: $1"
fi
;;
esac
done
[[ -z "$name" ]] && die "session name required (ais inspect <name>)"
require_session "$name"
local output
output=$(tmux capture-pane -t "$name" -p -S "-${lines}" 2>/dev/null | sanitize_utf8)
if [[ -z "$output" ]]; then
info "session '$name' has no output"
return
fi
echo "$output"
if [[ "$check_rate_limit" == true ]]; then
if echo "$output" | grep -qiE "$RATE_LIMIT_PATTERN"; then
echo ""
echo "*** RATE LIMIT DETECTED ***"
echo ""
echo "$output" | grep -iE "$RATE_LIMIT_PATTERN"
fi
fi
}
# ═══════════════════════════════════════════════════════════════════════
# cmd_inject
# ═══════════════════════════════════════════════════════════════════════
cmd_inject() {
local name="" text="" no_enter=false
while [[ $# -gt 0 ]]; do
case "$1" in
--no-enter) no_enter=true; shift ;;
-*) die "unknown option: $1" ;;
*)
if [[ -z "$name" ]]; then
name="$1"; shift
elif [[ -z "$text" ]]; then
text="$1"; shift
else
# Append remaining args as part of text
text="$text $1"; shift
fi
;;
esac
done
[[ -z "$name" ]] && die "session name required (ais inject <name> \"text\")"
[[ -z "$text" ]] && die "text required (ais inject <name> \"text\")"
require_session "$name"
tmux send-keys -t "$name" -l -- "$text"
if [[ "$no_enter" == false ]]; then
sleep 0.1
tmux send-keys -t "$name" Enter
fi
info "sent to '$name'"
}
# ═══════════════════════════════════════════════════════════════════════
# cmd_watch
# ═══════════════════════════════════════════════════════════════════════
cmd_watch() {
local name="" lines=30 interval=2 until_pattern=""
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--lines) lines="$2"; shift 2 ;;
-i|--interval) interval="$2"; shift 2 ;;
--until) until_pattern="$2"; shift 2 ;;
-*) die "unknown option: $1" ;;
*)
if [[ -z "$name" ]]; then
name="$1"; shift
else
die "unexpected argument: $1"
fi
;;
esac
done
[[ -z "$name" ]] && die "session name required (ais watch <name>)"
require_session "$name"
local agent account
agent=$(get_meta "$name" AIS_AGENT)
account=$(get_meta "$name" AIS_ACCOUNT)
trap 'echo ""; info "stopped watching"; exit 0' INT
while true; do
if ! session_exists "$name"; then
echo ""
info "session '$name' has ended"
break
fi
clear
printf "=== ais watch: %s | %s/%s | %s | Ctrl-C to stop ===\n\n" \
"$name" "${agent:-?}" "${account:-?}" "$(date +%H:%M:%S)"
local output
output=$(tmux capture-pane -t "$name" -p -S "-${lines}" 2>/dev/null | sanitize_utf8)
echo "$output"
# Rate limit check
if echo "$output" | grep -qiE "$RATE_LIMIT_PATTERN"; then
echo ""
echo "*** RATE LIMIT DETECTED ***"
fi
# Until pattern check
if [[ -n "$until_pattern" ]]; then
if echo "$output" | grep -qiE "$until_pattern"; then
echo ""
info "pattern matched: $until_pattern"
break
fi
fi
sleep "$interval"
done
}
# ═══════════════════════════════════════════════════════════════════════
# cmd_logs
# ═══════════════════════════════════════════════════════════════════════
cmd_logs() {
local name="" output_file=""
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output) output_file="$2"; shift 2 ;;
-*) die "unknown option: $1" ;;
*)
if [[ -z "$name" ]]; then
name="$1"; shift
else
die "unexpected argument: $1"
fi
;;
esac
done
[[ -z "$name" ]] && die "session name required (ais logs <name>)"
require_session "$name"
[[ -z "$output_file" ]] && output_file="${name}-$(date +%Y%m%d-%H%M%S).log"
# Capture entire scrollback (-S - means from the very beginning)
tmux capture-pane -t "$name" -p -S - 2>/dev/null | sanitize_utf8 > "$output_file"
local line_count
line_count=$(wc -l < "$output_file")
info "saved $line_count lines to $output_file"
}
# ═══════════════════════════════════════════════════════════════════════
# cmd_kill
# ═══════════════════════════════════════════════════════════════════════
cmd_kill() {
local name="" kill_all=false force=false save=false
while [[ $# -gt 0 ]]; do
case "$1" in
--all) kill_all=true; shift ;;
--force) force=true; shift ;;
--save) save=true; shift ;;
-*) die "unknown option: $1" ;;
*)
if [[ -z "$name" ]]; then
name="$1"; shift
else
die "unexpected argument: $1"
fi
;;
esac
done
if [[ "$kill_all" == true ]]; then
local sessions
sessions=$(tmux list-sessions -F '#{session_name}' 2>/dev/null) || true
[[ -z "$sessions" ]] && { info "no sessions to kill"; return; }
while IFS= read -r sess; do
is_managed "$sess" || continue
kill_one "$sess" "$force" "$save"
done <<< "$sessions"
return
fi
[[ -z "$name" ]] && die "session name required (ais kill <name> or ais kill --all)"
require_session "$name"
kill_one "$name" "$force" "$save"
}
kill_one() {
local name="$1" force="$2" save="$3"
if [[ "$save" == true ]]; then
local logfile="${name}-$(date +%Y%m%d-%H%M%S).log"
tmux capture-pane -t "$name" -p -S - 2>/dev/null | sanitize_utf8 > "$logfile"
info "saved scrollback to $logfile"
fi
if [[ "$force" == true ]]; then
tmux kill-session -t "$name" 2>/dev/null || true
info "killed '$name' (forced)"
return
fi
# Graceful: send /exit
tmux send-keys -t "$name" -l -- "/exit"
sleep 0.1
tmux send-keys -t "$name" Enter
# Wait up to 10 seconds
local i
for i in $(seq 1 10); do
session_exists "$name" || { info "killed '$name' (graceful)"; return; }
sleep 1
done
# Force kill
warn "'$name' did not exit gracefully, force killing"
tmux kill-session -t "$name" 2>/dev/null || true
info "killed '$name'"
}
# ═══════════════════════════════════════════════════════════════════════
# cmd_accounts
# ═══════════════════════════════════════════════════════════════════════
cmd_accounts() {
echo ""
echo " Claude Code accounts ($CLAUDE_ACCOUNTS_DIR)"
echo " ─────────────────────────────────────"
if [[ -d "$CLAUDE_ACCOUNTS_DIR" ]]; then
for d in "$CLAUDE_ACCOUNTS_DIR"/*/; do
[[ ! -d "$d" ]] && continue
local name
name=$(basename "$d")
printf " %-12s" "$name"
if [[ -f "$d/credentials.json" ]]; then
echo " (logged in)"
elif [[ -f "$d/settings.json" ]]; then
echo " (configured)"
else
echo " (empty)"
fi
done
else
echo " (none)"
fi
echo ""
echo " Kimi Code accounts ($KIMI_ACCOUNTS_DIR)"
echo " ─────────────────────────────────────"
if [[ -d "$KIMI_ACCOUNTS_DIR" ]]; then
for d in "$KIMI_ACCOUNTS_DIR"/*/; do
[[ ! -d "$d" ]] && continue
local name
name=$(basename "$d")
printf " %-12s" "$name"
if grep -q 'api_key = "sk-' "$d/config.toml" 2>/dev/null; then
echo " (API key)"
elif [[ -f "$d/credentials/kimi-code.json" ]]; then
echo " (OAuth)"
else
echo " (not configured)"
fi
done
else
echo " (none)"
fi
echo ""
}
# ═══════════════════════════════════════════════════════════════════════
# Main dispatch
# ═══════════════════════════════════════════════════════════════════════
case "${1:-help}" in
create) shift; cmd_create "$@" ;;
ls|list) shift; cmd_ls "$@" ;;
inspect|cap|capture) shift; cmd_inspect "$@" ;;
inject|send) shift; cmd_inject "$@" ;;
watch) shift; cmd_watch "$@" ;;
logs) shift; cmd_logs "$@" ;;
kill) shift; cmd_kill "$@" ;;
accounts|acct) shift; cmd_accounts "$@" ;;
-h|--help|help) usage ;;
-v|--version) echo "ais $VERSION" ;;
*) die "unknown command: $1 (try 'ais help')" ;;
esac
#!/bin/bash
# check-credit.sh — Check Claude Code credit usage for all accounts
# Launches a temp Claude Code session per account, runs /usage, parses output
# Times displayed in GMT+7 (Bangkok)
#
# Usage: bash check-credit.sh
# bash check-credit.sh cc3 hataricc # check specific accounts only
set -uo pipefail
CLAUDE_BIN=~/.npm/bin/claude
ACCOUNTS_DIR=~/.claude-accounts
TMUX_SESSION="credit-check-tmp"
WAIT_LOAD=14 # seconds to wait for Claude to load
WAIT_USAGE=8 # seconds to wait for /usage output
# If specific accounts passed as args, use those; otherwise check all
if [ $# -gt 0 ]; then
ACCOUNTS=("$@")
else
ACCOUNTS=(cc1 cc2 cc3 hataricc nicxxx)
fi
# ============================================================================
# UTC to GMT+7 converter
# ============================================================================
convert_to_gmt7() {
local time_str="$1"
# Handle various formats from Claude Code /usage output
# "Resets 12am (UTC)" -> today at 00:00 UTC
# "Resets 8pm (UTC)" -> today at 20:00 UTC
# "Resets 9am (UTC)" -> today at 09:00 UTC
# "Resets Feb 28, 2am (UTC)" -> Feb 28 at 02:00 UTC
# "Resets Mar 1, 6:59am (UTC)" -> Mar 1 at 06:59 UTC
# "Resets Mar 1, 2pm (UTC)" -> Mar 1 at 14:00 UTC
# "Resets Mar 4, 4am (UTC)" -> Mar 4 at 04:00 UTC
# Extract the time string after "Resets "
local reset_part
reset_part=$(echo "$time_str" | sed 's/.*Resets //' | sed 's/ (UTC).*//' | sed 's/[[:space:]]*$//')
if [ -z "$reset_part" ]; then
echo "$time_str"
return
fi
# Try to parse with GNU date
local utc_epoch=""
local year
year=$(date +%Y)
# Check if it has a date component (month name present)
if echo "$reset_part" | grep -qE '^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)'; then
# Format: "Mar 1, 6:59am" or "Feb 28, 2am" or "Mar 1, 2pm"
local month_day time_part
month_day=$(echo "$reset_part" | sed 's/,.*//')
time_part=$(echo "$reset_part" | sed 's/.*,[ ]*//')
# Normalize time: "2am" -> "2:00am", "6:59am" stays "6:59am", "2pm" -> "2:00pm"
if ! echo "$time_part" | grep -q ':'; then
time_part=$(echo "$time_part" | sed 's/\([0-9]\+\)\([ap]m\)/\1:00\2/')
fi
# Convert 12h to 24h via date
utc_epoch=$(date -u -d "$month_day $year $time_part UTC" +%s 2>/dev/null || echo "")
else
# Format: "12am" or "8pm" or "9am" — today's date
local time_only="$reset_part"
if ! echo "$time_only" | grep -q ':'; then
time_only=$(echo "$time_only" | sed 's/\([0-9]\+\)\([ap]m\)/\1:00\2/')
fi
utc_epoch=$(date -u -d "today $time_only UTC" +%s 2>/dev/null || echo "")
fi
if [ -n "$utc_epoch" ]; then
# Add 7 hours (25200 seconds) for GMT+7
local gmt7_epoch=$((utc_epoch + 25200))
local gmt7_str
gmt7_str=$(date -u -d "@$gmt7_epoch" "+%b %d, %I:%M%p" 2>/dev/null | sed 's/AM/am/;s/PM/pm/')
echo "$gmt7_str (GMT+7)"
else
echo "$time_str (UTC)"
fi
}
# ============================================================================
# Check one account
# ============================================================================
check_account() {
local acct="$1"
local config_dir="$ACCOUNTS_DIR/$acct"
if [ ! -d "$config_dir" ]; then
echo "SKIP|$acct|Directory not found|–|–|–"
return
fi
# Kill any leftover session
tmux kill-session -t "$TMUX_SESSION" 2>/dev/null || true
# Start Claude Code with the account's config dir
tmux new-session -d -s "$TMUX_SESSION" -x 160 -y 50
tmux send-keys -t "$TMUX_SESSION" "CLAUDE_CONFIG_DIR=$config_dir $CLAUDE_BIN" Enter
# Wait for Claude to load
sleep "$WAIT_LOAD"
# Check if we hit a login screen or bypass permissions prompt
local screen
screen=$(tmux capture-pane -t "$TMUX_SESSION" -p -S -30 2>/dev/null || true)
if echo "$screen" | grep -qiE "Select login method|log in"; then
tmux kill-session -t "$TMUX_SESSION" 2>/dev/null || true
echo "AUTH|$acct|Not logged in|–|–|–"
return
fi
# If bypass permissions prompt appears, accept it
if echo "$screen" | grep -qi "Yes, I accept"; then
tmux send-keys -t "$TMUX_SESSION" Down Enter
sleep 10
screen=$(tmux capture-pane -t "$TMUX_SESSION" -p -S -30 2>/dev/null || true)
# If it exited back to shell, try without bypass
if echo "$screen" | grep -q '^\$\|^villa@'; then
tmux send-keys -t "$TMUX_SESSION" "CLAUDE_CONFIG_DIR=$config_dir $CLAUDE_BIN" Enter
sleep "$WAIT_LOAD"
fi
fi
# Send /usage command
tmux send-keys -t "$TMUX_SESSION" '/usage' Enter
sleep 2
# Press Enter to select from command picker
tmux send-keys -t "$TMUX_SESSION" Enter
sleep "$WAIT_USAGE"
# Capture the usage output
local output
output=$(tmux capture-pane -t "$TMUX_SESSION" -p -S -40 2>/dev/null || true)
# Parse usage data
local week_all week_sonnet session_used
local reset_all reset_sonnet
# Extract weekly all-models percentage
week_all=$(echo "$output" | grep -A2 "Current week (all models)" | grep -oP '\d+(?=% used)' | head -1)
[ -z "$week_all" ] && week_all="?"
# Extract Sonnet percentage
week_sonnet=$(echo "$output" | grep -A2 "Sonnet only" | grep -oP '\d+(?=% used)' | head -1)
[ -z "$week_sonnet" ] && week_sonnet="0"
# Extract session percentage
session_used=$(echo "$output" | grep -A2 "Current session" | grep -oP '\d+(?=% used)' | head -1)
[ -z "$session_used" ] && session_used="0"
# Extract reset times
reset_all=$(echo "$output" | grep -A3 "Current week (all models)" | grep "Resets" | head -1 | sed 's/^[[:space:]]*//')
reset_sonnet=$(echo "$output" | grep -A3 "Sonnet only" | grep "Resets" | head -1 | sed 's/^[[:space:]]*//')
# Convert reset times to GMT+7
local reset_all_gmt7 reset_sonnet_gmt7
if [ -n "$reset_all" ]; then
reset_all_gmt7=$(convert_to_gmt7 "$reset_all")
else
reset_all_gmt7="–"
fi
# Clean up
tmux send-keys -t "$TMUX_SESSION" Escape 2>/dev/null
sleep 1
tmux send-keys -t "$TMUX_SESSION" '/exit' Enter 2>/dev/null
sleep 2
tmux kill-session -t "$TMUX_SESSION" 2>/dev/null || true
echo "OK|$acct|$week_all|$week_sonnet|$((100 - week_all))|$reset_all_gmt7"
}
# ============================================================================
# Main
# ============================================================================
echo ""
echo "╔══════════════════════════════════════════════════════════════════════╗"
echo "║ Claude Code Credit Check (GMT+7) ║"
echo "║ Checking ${#ACCOUNTS[@]} accounts... ║"
echo "╚══════════════════════════════════════════════════════════════════════╝"
echo ""
# Collect results
declare -a RESULTS
for acct in "${ACCOUNTS[@]}"; do
printf " Checking %-12s ... " "$acct"
result=$(check_account "$acct")
RESULTS+=("$result")
status=$(echo "$result" | cut -d'|' -f1)
case "$status" in
OK) printf "done\n" ;;
AUTH) printf "NOT LOGGED IN\n" ;;
SKIP) printf "skipped\n" ;;
esac
done
echo ""
echo "┌──────────────┬──────────┬──────────┬───────────┬──────────────────────────┐"
echo "│ Account │ Week All │ Sonnet │ Remaining │ Resets (GMT+7) │"
echo "├──────────────┼──────────┼──────────┼───────────┼──────────────────────────┤"
for result in "${RESULTS[@]}"; do
status=$(echo "$result" | cut -d'|' -f1)
acct=$(echo "$result" | cut -d'|' -f2)
if [ "$status" = "AUTH" ]; then
printf "│ %-12s │ %-7s │ %-7s │ %-8s │ %-24s │\n" "$acct" "–" "–" "NO AUTH" "Needs login"
elif [ "$status" = "SKIP" ]; then
printf "│ %-12s │ %-7s │ %-7s │ %-8s │ %-24s │\n" "$acct" "–" "–" "SKIP" "Not found"
else
week_all=$(echo "$result" | cut -d'|' -f3)
week_sonnet=$(echo "$result" | cut -d'|' -f4)
remaining=$(echo "$result" | cut -d'|' -f5)
resets=$(echo "$result" | cut -d'|' -f6)
# Color coding
bar=""
if [ "$remaining" -le 0 ] 2>/dev/null; then
bar="EMPTY"
elif [ "$remaining" -le 20 ] 2>/dev/null; then
bar="LOW"
elif [ "$remaining" -le 50 ] 2>/dev/null; then
bar="MED"
else
bar="OK"
fi
printf "│ %-12s │ %3s%% │ %3s%% │ %3s%% %-3s │ %-24s │\n" \
"$acct" "$week_all" "$week_sonnet" "$remaining" "$bar" "$resets"
fi
done
echo "└──────────────┴──────────┴──────────┴───────────┴──────────────────────────┘"
echo ""
# Summary recommendation
best_acct=""
best_remaining=0
for result in "${RESULTS[@]}"; do
status=$(echo "$result" | cut -d'|' -f1)
[ "$status" != "OK" ] && continue
acct=$(echo "$result" | cut -d'|' -f2)
remaining=$(echo "$result" | cut -d'|' -f5)
if [ "$remaining" -gt "$best_remaining" ] 2>/dev/null; then
best_remaining="$remaining"
best_acct="$acct"
fi
done
if [ -n "$best_acct" ]; then
echo " Best account: $best_acct ($best_remaining% remaining)"
echo " To use: gt crew start <crew-name> --account $best_acct"
echo ""
fi
#!/bin/bash
# check-credits — Quick credit check for all Claude Code accounts
# Outputs inline. Works from terminal and when called by agents.
#
# Usage: check-credits # all accounts
# check-credits cc1 cc3 # specific accounts
#
# Output is always saved to /tmp/credit-check-latest.txt
SCRIPT="$HOME/gt/villa_backend_4/crew/dev/scripts/nudge/check-credit.sh"
OUTFILE="/tmp/credit-check-latest.txt"
# Run the check, capturing output (tmux messes with direct stdout)
bash "$SCRIPT" "$@" > "$OUTFILE" 2>&1
# Print results — use /dev/tty if available, else just cat
if [ -t 1 ]; then
cat "$OUTFILE"
else
# When stdout is piped/captured (e.g. from an agent), cat works fine
cat "$OUTFILE"
fi
# Always exit with useful info for agents that can't see stdout
echo ""
echo "Results saved to: $OUTFILE"
name description metadata
ais-orchestrator
Orchestrate many Claude Code and Kimi Code sub-agents in parallel via tmux. Use when: decomposing large tasks into parallel work, managing multiple coding agents, monitoring agent health, rotating accounts on rate limits. Requires: ais CLI (~/.local/bin/ais), tmux, kimi-account (~/.local/bin/kimi-account).
openclaw
emoji os requires
🕸️
linux
darwin
bins
ais
tmux

AI Session Orchestrator

Manage a fleet of Claude Code and Kimi Code sub-agents running in parallel tmux sessions. Each agent gets its own account, working directory, and task. You monitor them, detect failures, rotate accounts on rate limits, and collect results.

Prerequisites

  • ais CLI installed at ~/.local/bin/ais
  • kimi-account CLI installed at ~/.local/bin/kimi-account
  • tmux installed
  • Claude Code accounts configured in ~/.claude-accounts/
  • Kimi Code accounts configured in ~/.kimi-accounts/

Account Inventory

Check what's available before spawning agents:

ais accounts

Claude Code Accounts

Account Model Best For
cc1 Sonnet Fast tasks, polecat workers
cc2 Sonnet Fast tasks, parallel workers
cc3 Opus Complex reasoning, planning
hataricc Opus Complex tasks, fallback
nicxxx Opus Complex tasks, fallback

Env var: CLAUDE_CONFIG_DIR=~/.claude-accounts/<name>

Kimi Code Accounts

Run kimi-account list to see all. Kimi is free (no credit cost) but has rate limits.

Account Auth Best For
1 OAuth Interactive, browser-login
2 API key Servers, headless agents
3+ Varies Add more with kimi-account add <key>

Env var: KIMI_SHARE_DIR=~/.kimi-accounts/<number>


Core Workflow

Step 1: Decompose the Task

Break the user's request into independent subtasks that can run in parallel. Each subtask should:

  • Be self-contained (no dependency on other subtasks)
  • Have a clear, specific objective
  • Target a specific directory or set of files
  • Be completable in one session

Example decomposition:

User: "Fix all failing tests and add missing error handling across the backend"

Subtasks:
1. Fix order-service tests         → agent: claude/cc1, dir: services/order-service/
2. Fix product-service tests       → agent: claude/cc2, dir: services/product-service/
3. Fix payment-service tests       → agent: kimi/1,     dir: services/payment-service/
4. Add error handling to auth      → agent: claude/cc3, dir: services/auth-service/
5. Add error handling to shipping  → agent: kimi/2,     dir: services/shipping-service/

Step 2: Spawn Sub-Agents

Use ais create for each subtask. Spread across accounts to avoid rate limits.

# Claude Code agents (use --yolo for auto-approve)
ais create fix-orders -a claude -A cc1 --yolo -d ~/project/services/order-service \
  -c "Fix all failing tests. Run pytest and fix until all pass."

ais create fix-products -a claude -A cc2 --yolo -d ~/project/services/product-service \
  -c "Fix all failing tests. Run pytest and fix until all pass."

# Kimi Code agents (--yolo built in)
ais create fix-payments -a kimi -A 1 --yolo -d ~/project/services/payment-service \
  -c "Fix all failing tests. Run pytest and fix until all pass."

ais create add-auth-errors -a claude -A cc3 --yolo -d ~/project/services/auth-service \
  -c "Add proper error handling to all Lambda handlers. Use villa_common.exceptions."

ais create add-ship-errors -a kimi -A 2 --yolo -d ~/project/services/shipping-service \
  -c "Add proper error handling to all Lambda handlers."

Step 3: Monitor All Agents

# List all running agents
ais ls

# Quick health check on all
for name in fix-orders fix-products fix-payments add-auth-errors add-ship-errors; do
  echo "=== $name ==="
  ais inspect "$name" -n 10 --rate-limit 2>/dev/null || echo "[dead]"
  echo ""
done

Step 4: Detect and Recover from Problems

# Check for rate limits
ais inspect fix-orders --rate-limit

# If rate limited, kill and respawn with different account
ais kill fix-orders
ais create fix-orders -a claude -A cc3 --yolo -d ~/project/services/order-service \
  -c "Fix all failing tests. Run pytest and fix until all pass."

Step 5: Collect Results

# Capture final output from each agent
ais inspect fix-orders -n 100 > /tmp/results-orders.txt
ais inspect fix-products -n 100 > /tmp/results-products.txt

# Save full scrollback logs
ais logs fix-orders -o /tmp/fix-orders-full.log
ais logs fix-products -o /tmp/fix-products-full.log

Step 6: Clean Up

# Kill all managed sessions
ais kill --all

# Or kill individually with log save
ais kill fix-orders --save
ais kill fix-products --save

Problem Detection & Recovery Playbook

Rate Limit Detected

Detection: ais inspect <name> --rate-limit prints *** RATE LIMIT DETECTED ***

Recovery:

  1. Note which account is rate-limited
  2. Kill the session: ais kill <name>
  3. Pick a different account from the same agent type
  4. Respawn: ais create <name> -a <agent> -A <new-account> ...

Account rotation order:

  • Claude: cc1 → cc2 → cc3 → hataricc → nicxxx → cc1
  • Kimi: 1 → 2 → 3 → ... → 1
# Example rotation
ais kill worker1
ais create worker1 -a claude -A cc2 --yolo -d ~/project -c "continue the previous task"

Agent Crashed / Session Dead

Detection: ais ls doesn't show the session, or ais inspect <name> returns error

Recovery:

  1. Check if session exists: tmux has-session -t <name> 2>/dev/null && echo alive || echo dead
  2. If dead, respawn with same account: ais create <name> -a <agent> -A <account> ...
  3. If the crash was due to a bug in the task, adjust the prompt

Agent Stuck / Idle

Detection: ais inspect <name> -n 20 shows no recent activity, or shows a prompt waiting for input

Recovery:

  1. Try sending Enter to unstick: ais inject <name> ""
  2. If waiting for a yes/no prompt: ais inject <name> "y"
  3. If truly stuck, kill and respawn with a clearer prompt

Authentication Error

Detection: Output contains unauthorized, 401, 403, invalid key, token expired

Recovery for Claude: Re-login the account (CLAUDE_CONFIG_DIR=~/.claude-accounts/<name> claude /login) Recovery for Kimi API key: Key is permanent, shouldn't expire. Check config. Recovery for Kimi OAuth: Re-login: kimi-account login <number>

Connection Error

Detection: Output contains ECONNREFUSED, timeout, network error, connection reset

Recovery:

  1. Wait 30 seconds, check again
  2. If persistent, kill and respawn (network issue may have resolved)
  3. If all agents have connection errors, it's likely a network or service outage — wait and retry

Monitoring Patrol Loop

For long-running orchestration, run a patrol loop:

#!/bin/bash
# patrol.sh — monitor all ais agents every 2 minutes
INTERVAL=120
RATE_LIMIT_PATTERN='rate.?limit|429|overloaded|quota.?exceeded|too many requests|credit balance is too low|insufficient_quota|hit your limit'

while true; do
  echo "=== Patrol $(date +%H:%M:%S) ==="

  for sess in $(ais ls 2>/dev/null | tail -n +3 | awk '{print $1}'); do
    [ -z "$sess" ] && continue

    # Check if alive
    if ! tmux has-session -t "$sess" 2>/dev/null; then
      echo "  $sess: DEAD"
      continue
    fi

    # Check for rate limits
    output=$(ais inspect "$sess" -n 50 2>/dev/null)
    if echo "$output" | grep -qiE "$RATE_LIMIT_PATTERN"; then
      echo "  $sess: RATE LIMITED — needs account rotation"
    else
      # Show last meaningful line
      last_line=$(echo "$output" | grep -v '^$' | tail -1)
      echo "  $sess: alive — $last_line"
    fi
  done

  echo ""
  sleep "$INTERVAL"
done

Agent Prompt Templates

Claude Code Sub-Agent Prompt

When spawning Claude Code agents, inject a prompt that:

  1. States the specific task clearly
  2. Sets boundaries (which files/dirs to touch)
  3. Requests a summary when done
Fix all failing tests in this service directory.

Steps:
1. Run: python3 -m pytest tests/ -v
2. Read failing test output carefully
3. Fix the code (not the tests) to make them pass
4. Re-run tests to confirm all pass
5. When done, print: TASK COMPLETE: <number of tests fixed>

Do NOT modify files outside this directory.

Kimi Code Sub-Agent Prompt

Kimi works similarly but has different strengths (free, good at code generation):

Add comprehensive error handling to all Lambda handlers in this service.

For each handler:
1. Wrap the main logic in try/except
2. Use villa_common.exceptions (ValidationError, NotFoundError)
3. Return proper HTTP status codes using villa_common.response helpers
4. Log errors with the Lambda context request_id

When done, print: TASK COMPLETE: <number of handlers updated>

Capacity Planning

Agent Type Max Concurrent Notes
Claude Code (Opus) 3 cc3, hataricc, nicxxx — shared rate limit pool
Claude Code (Sonnet) 2 cc1, cc2 — faster but less capable
Kimi Code 2+ Free, add more with kimi-account add <key>
Total ~7 Mix Claude + Kimi for maximum parallelism

Strategy: Use Kimi for simpler tasks (test fixing, boilerplate, error handling). Use Claude Opus for complex tasks (architecture, debugging, multi-file refactors). Use Claude Sonnet for medium tasks.


Quick Reference

# === LIFECYCLE ===
ais create <name> -a claude|kimi -A <account> [--yolo] [-c "cmd"] [-d dir]
ais ls                              # list all sessions
ais kill <name>                     # graceful shutdown
ais kill --all                      # kill everything

# === MONITORING ===
ais inspect <name> -n 100           # capture last 100 lines
ais inspect <name> --rate-limit     # check for rate limits
ais watch <name> -i 5               # live tail (every 5s)
ais logs <name> -o file.log         # save full scrollback

# === INTERACTION ===
ais inject <name> "do the thing"    # send command
ais inject <name> "y"               # answer a prompt

# === ACCOUNTS ===
ais accounts                        # list all accounts
kimi-account list                   # list kimi accounts
kimi-account check                  # test all kimi accounts
kimi-account add sk-kimi-XXX        # add new kimi account

Rules

  1. Always check ais accounts before spawning — know what's available
  2. Spread across accounts — don't put 5 agents on the same account
  3. Monitor regularly — check every 2-5 minutes for problems
  4. Rotate on rate limit — don't wait, immediately kill and respawn with different account
  5. Use Kimi for simple tasks — it's free, save Claude credits for hard problems
  6. Save logs before killingais kill <name> --save preserves evidence
  7. Never send Ctrl-C — always use ais kill for graceful shutdown
  8. Keep the user informed — report what's running, what finished, what failed

Controlling AI Agents via Tmux

A practical guide for programmatically creating, controlling, and monitoring Claude Code and Kimi Code sessions through tmux. All patterns are battle-tested from production patrol/nudge automation.

Quick Start: ais CLI

The ais (AI Sessions) tool wraps all the patterns below into a single command:

# Install
curl -sL <gist-raw-url>/ais.sh -o ~/.local/bin/ais && chmod +x ~/.local/bin/ais

# Create a session
ais create worker1 -a kimi -A 1 -c "fix the tests"    # kimi account 1
ais create reviewer -a claude -A cc3 --yolo             # claude account cc3

# Monitor & interact
ais ls                              # list all sessions
ais inspect worker1 -n 100          # capture last 100 lines
ais inspect worker1 --rate-limit    # check for rate limits
ais inject worker1 "run the tests"  # send command
ais watch worker1 -i 5              # live monitor (every 5s)
ais logs worker1 -o session.log     # save full scrollback

# Manage
ais kill worker1                    # graceful shutdown
ais kill --all                      # kill all managed sessions
ais accounts                        # list available accounts

The rest of this document explains the raw tmux patterns that ais uses under the hood.


Table of Contents


Core Concepts

Both Claude Code and Kimi Code are interactive TUI (terminal UI) applications. To automate them:

  1. Create a detached tmux session
  2. Launch the agent inside it (with account env vars)
  3. Send commands via tmux send-keys
  4. Capture output via tmux capture-pane
  5. Parse the captured text for status, errors, rate limits
  6. Exit gracefully when done

Account Isolation

Each tool uses an environment variable to point to an isolated config/credentials directory.

Claude Code

# Env var: CLAUDE_CONFIG_DIR
# Default: ~/.claude
# Accounts live in: ~/.claude-accounts/<name>/

# Launch Claude Code with a specific account
CLAUDE_CONFIG_DIR=~/.claude-accounts/cc1 claude

# Each account dir contains:
# ~/.claude-accounts/cc1/
# ├── settings.json
# ├── credentials.json
# ├── projects/
# └── ...

Kimi Code

# Env var: KIMI_SHARE_DIR
# Default: ~/.kimi
# Accounts live in: ~/.kimi-accounts/<number>/

# Launch Kimi with a specific account
KIMI_SHARE_DIR=~/.kimi-accounts/1 kimi

# Or use the wrapper
kimi-account 1

# Each account dir contains:
# ~/.kimi-accounts/1/
# ├── config.toml          # API key or OAuth config
# ├── credentials/          # OAuth tokens (if OAuth)
# └── sessions/

OpenCode (uses Kimi credentials)

# OpenCode respects the same KIMI_SHARE_DIR
KIMI_SHARE_DIR=~/.kimi-accounts/1 opencode

# Or use the wrapper
kimi-account opencode 1

Session Lifecycle

Create a Session

SESSION="my-agent"

# Kill existing session if any (idempotent start)
tmux kill-session -t "$SESSION" 2>/dev/null || true

# Create detached session with explicit dimensions
# Width 160+ avoids line-wrap issues in captured output
tmux new-session -d -s "$SESSION" -x 160 -y 50

Launch Agent in Session

# Claude Code with account isolation
tmux send-keys -t "$SESSION" \
  "CLAUDE_CONFIG_DIR=~/.claude-accounts/cc1 claude" Enter

# Kimi Code with account isolation
tmux send-keys -t "$SESSION" \
  "KIMI_SHARE_DIR=~/.kimi-accounts/1 kimi" Enter

# Kimi Code with specific flags
tmux send-keys -t "$SESSION" \
  "KIMI_SHARE_DIR=~/.kimi-accounts/2 kimi --yolo" Enter

# OpenCode
tmux send-keys -t "$SESSION" \
  "KIMI_SHARE_DIR=~/.kimi-accounts/1 opencode" Enter

# IMPORTANT: Wait for the agent to fully start before sending commands
sleep 12  # Claude Code needs ~10-15 seconds to initialize
sleep 8   # Kimi Code needs ~5-10 seconds

Exit Gracefully

# Claude Code: send /exit slash command
tmux send-keys -t "$SESSION" '/exit' Enter
sleep 3

# Kimi Code: send /exit or Ctrl-D
tmux send-keys -t "$SESSION" '/exit' Enter
sleep 3

# If the session is still alive, kill it
tmux kill-session -t "$SESSION" 2>/dev/null || true

Check if Session Exists

if tmux has-session -t "$SESSION" 2>/dev/null; then
  echo "Session is running"
else
  echo "Session is dead"
fi

Sending Commands

Basic Text Input

# Send a prompt/question to the agent
tmux send-keys -t "$SESSION" "fix the failing tests in auth.py" Enter

# Send a slash command
tmux send-keys -t "$SESSION" "/status" Enter

# Send just Enter (to accept a prompt, unstick a waiting agent)
tmux send-keys -t "$SESSION" Enter

Navigating TUI Menus

Some commands (like Claude's /usage) open a picker menu:

# Send the command
tmux send-keys -t "$SESSION" '/usage' Enter
sleep 2

# Navigate down in the picker and select
tmux send-keys -t "$SESSION" Down Enter
sleep 5  # Wait for output to render

Multi-Step Interactions

# Example: Check Claude Code credit usage
WAIT_LOAD=14    # Startup wait
WAIT_USAGE=8    # /usage response wait

# 1. Launch
tmux send-keys -t "$SESSION" \
  "CLAUDE_CONFIG_DIR=~/.claude-accounts/cc1 claude" Enter
sleep "$WAIT_LOAD"

# 2. Send /usage command
tmux send-keys -t "$SESSION" '/usage' Enter
sleep 2

# 3. Select first option in picker
tmux send-keys -t "$SESSION" Enter
sleep "$WAIT_USAGE"

# 4. Now capture the output (see next section)

Timing Rules

Action Wait After
Launch Claude Code 12-15 seconds
Launch Kimi Code 5-10 seconds
Send slash command 2-3 seconds
Send text prompt 1-2 seconds (before capture)
/usage response 5-8 seconds
/exit command 2-3 seconds
Navigate menu (Down/Enter) 1-2 seconds

Capturing Output

Basic Capture

# Capture the last 200 lines of output
OUTPUT=$(tmux capture-pane -t "$SESSION" -p -S -200 2>/dev/null)

# Flags:
#   -t SESSION   target session
#   -p           print to stdout (instead of paste buffer)
#   -S -200      start 200 lines back in scrollback history
#   2>/dev/null  suppress error if session doesn't exist

Capture Depth Guidelines

Purpose Depth Why
Quick status check -S -50 Recent activity only
Agent status/idle detection -S -200 Need enough context to detect stalls
Rate limit detection -S -500 Rate limit messages can scroll up fast
Full session review -S -1000 Deep history for debugging

UTF-8 Sanitization (Critical)

Tmux captures can include invalid UTF-8 bytes, ANSI escape codes, and non-printable characters that break downstream parsers (JSON, Python, etc.). Always sanitize:

sanitize_utf8() {
  LC_ALL=C sed 's/[^[:print:][:space:]]//g' | \
    iconv -f utf-8 -t utf-8 -c 2>/dev/null || cat
}

# Usage
OUTPUT=$(tmux capture-pane -t "$SESSION" -p -S -200 2>/dev/null \
  | tail -200 \
  | sanitize_utf8)

Trimming Output

When feeding captured output to another AI (e.g., Kimi for analysis), trim to avoid overwhelming the context:

# Trim to first 2000 characters
TRIMMED=$(echo "$OUTPUT" | head -c 2000)

# Or trim to last N lines
RECENT=$(echo "$OUTPUT" | tail -50)

Multi-Agent Capture

# Capture all crew agents at once
SESSIONS="agent-1 agent-2 agent-3"
ALL_STATUS=""

for sess in $SESSIONS; do
  PANE_OUT=$(tmux capture-pane -t "$sess" -p -S -200 2>/dev/null \
    | tail -200 | sanitize_utf8 || echo "[session not found]")

  if [ -z "$PANE_OUT" ] || [ "$PANE_OUT" = "[session not found]" ]; then
    ALL_STATUS+="$sess: [NOT RUNNING]\n"
  else
    TRIMMED=$(echo "$PANE_OUT" | head -c 2000)
    ALL_STATUS+="--- $sess ---\n$TRIMMED\n\n"
  fi
done

echo -e "$ALL_STATUS"

Detecting Problems

Rate Limits

OUTPUT=$(tmux capture-pane -t "$SESSION" -p -S -500 2>/dev/null)

if echo "$OUTPUT" | grep -qiE \
  "rate.?limit|429|overloaded|quota.?exceeded|too many requests|credit balance is too low|insufficient_quota|hit your limit"; then
  echo "RATE LIMITED"
fi

Patterns to detect:

Pattern Meaning
rate.?limit Generic rate limit
429 HTTP 429 Too Many Requests
overloaded Service overloaded
quota.?exceeded API quota exhausted
too many requests Rate limit message
credit balance is too low Claude Code credits gone
insufficient_quota API quota error
hit your limit Generic limit hit

Stalled / Idle Agent

OUTPUT=$(tmux capture-pane -t "$SESSION" -p -S -100 2>/dev/null)

# Check for idle indicators
if echo "$OUTPUT" | grep -qiE \
  "waiting.*instruction|standing by|no work|nothing to do"; then
  echo "AGENT IDLE"
fi

# Check for error states
if echo "$OUTPUT" | grep -qiE \
  "error|crash|failed|exception|traceback"; then
  echo "AGENT ERROR"
fi

Authentication Errors

if echo "$OUTPUT" | grep -qiE \
  "auth|unauthorized|invalid.*key|401|403|token.*expired|login.*required"; then
  echo "AUTH ERROR"
fi

Testing if Agent Responds

# Quick test: send a simple prompt and check response
KIMI_SHARE_DIR=~/.kimi-accounts/1 timeout 20 kimi \
  -c "Say just the word 'ok'" --quiet 2>/dev/null

# Evaluate response
RESULT=$?
if [ $RESULT -eq 0 ]; then
  echo "WORKING"
elif [ $RESULT -eq 124 ]; then
  echo "TIMEOUT"
else
  echo "FAILED"
fi

Common Recipes

Recipe 1: One-Shot Command with Result

Run a command, capture the output, clean up.

#!/bin/bash
SESSION="oneshot-$$"
ACCOUNT_DIR=~/.kimi-accounts/1
PROMPT="list all files in the current directory"

# Create session and launch
tmux new-session -d -s "$SESSION" -x 160 -y 50
tmux send-keys -t "$SESSION" \
  "KIMI_SHARE_DIR=$ACCOUNT_DIR kimi -c '$PROMPT' --quiet" Enter

# Wait for completion (adjust timeout as needed)
sleep 30

# Capture result
RESULT=$(tmux capture-pane -t "$SESSION" -p -S -200 2>/dev/null | sanitize_utf8)

# Clean up
tmux kill-session -t "$SESSION" 2>/dev/null || true

echo "$RESULT"

Recipe 2: Interactive Agent with Periodic Monitoring

Launch an agent and check on it every N minutes.

#!/bin/bash
SESSION="my-worker"
CHECK_INTERVAL=300  # 5 minutes

# Launch
tmux kill-session -t "$SESSION" 2>/dev/null || true
tmux new-session -d -s "$SESSION" -x 160 -y 50
tmux send-keys -t "$SESSION" \
  "CLAUDE_CONFIG_DIR=~/.claude-accounts/cc1 claude --yolo" Enter
sleep 15

# Monitor loop
while true; do
  sleep "$CHECK_INTERVAL"

  # Check if session still exists
  if ! tmux has-session -t "$SESSION" 2>/dev/null; then
    echo "Session died. Restarting..."
    tmux new-session -d -s "$SESSION" -x 160 -y 50
    tmux send-keys -t "$SESSION" \
      "CLAUDE_CONFIG_DIR=~/.claude-accounts/cc1 claude --yolo" Enter
    sleep 15
    continue
  fi

  # Capture and check for problems
  OUTPUT=$(tmux capture-pane -t "$SESSION" -p -S -500 2>/dev/null | sanitize_utf8)

  if echo "$OUTPUT" | grep -qiE "rate.?limit|429|credit balance"; then
    echo "Rate limited! Consider rotating account."
    # See Account Rotation section below
  fi
done

Recipe 3: Send a Nudge to Running Agent

# Send a message/instruction to a running Claude Code session
SESSION="my-worker"
MSG="Please check the test results and fix any failures"

# Just type the message into the agent's input
tmux send-keys -t "$SESSION" "$MSG" Enter

Recipe 4: Check All Claude Accounts

#!/bin/bash
ACCOUNTS=(cc1 cc2 cc3 hataricc nicxxx)
SESSION="credit-check-tmp"
CLAUDE_BIN=~/.claude/local/claude

for acct in "${ACCOUNTS[@]}"; do
  config_dir=~/.claude-accounts/$acct

  tmux kill-session -t "$SESSION" 2>/dev/null || true
  tmux new-session -d -s "$SESSION" -x 160 -y 50

  tmux send-keys -t "$SESSION" \
    "CLAUDE_CONFIG_DIR=$config_dir $CLAUDE_BIN" Enter
  sleep 14

  tmux send-keys -t "$SESSION" '/usage' Enter
  sleep 2
  tmux send-keys -t "$SESSION" Enter
  sleep 8

  OUTPUT=$(tmux capture-pane -t "$SESSION" -p -S -100 2>/dev/null)
  echo "=== $acct ==="
  echo "$OUTPUT" | grep -iE "percent|usage|credit|limit|reset" || echo "(no usage data found)"
  echo ""

  tmux send-keys -t "$SESSION" '/exit' Enter
  sleep 2
  tmux kill-session -t "$SESSION" 2>/dev/null || true
done

Recipe 5: Check All Kimi Accounts

#!/bin/bash
ACCOUNTS_DIR=~/.kimi-accounts
KIMI_BIN=~/.local/bin/kimi

for d in "$ACCOUNTS_DIR"/*/; do
  [ ! -d "$d" ] && continue
  acct=$(basename "$d")
  printf "  %-4s  " "$acct"

  result=$(KIMI_SHARE_DIR="$d" timeout 20 "$KIMI_BIN" \
    -c "Say just the word 'ok'" --quiet 2>/dev/null || echo "FAILED")

  if echo "$result" | grep -qi "ok\|yes\|hello"; then
    echo "WORKING"
  elif echo "$result" | grep -qi "rate.limit\|429"; then
    echo "RATE LIMITED"
  elif echo "$result" | grep -qi "auth\|unauthorized\|401"; then
    echo "BAD KEY"
  else
    echo "NO RESPONSE"
  fi
done

Recipe 6: Run Multiple Agents in Parallel

#!/bin/bash
# Launch 3 Kimi agents on different accounts, each in their own tmux session

AGENTS=("worker-1:1" "worker-2:2" "worker-3:3")  # session:account

for entry in "${AGENTS[@]}"; do
  sess="${entry%%:*}"
  acct="${entry##*:}"

  tmux kill-session -t "$sess" 2>/dev/null || true
  tmux new-session -d -s "$sess" -x 160 -y 50
  tmux send-keys -t "$sess" \
    "KIMI_SHARE_DIR=~/.kimi-accounts/$acct kimi --yolo" Enter
done

echo "Launched ${#AGENTS[@]} agents. Use 'tmux ls' to see sessions."
echo "Attach: tmux attach -t worker-1"
echo "Monitor: tmux capture-pane -t worker-1 -p -S -50"

Account Rotation

When an agent hits a rate limit, rotate to a different account.

Round-Robin Selection

ALL_ACCOUNTS=(cc1 cc2 cc3 hataricc nicxxx)

get_next_account() {
  local current="$1"
  local found=false

  for acct in "${ALL_ACCOUNTS[@]}"; do
    if [ "$found" = true ]; then
      echo "$acct"
      return
    fi
    [ "$acct" = "$current" ] && found=true
  done

  # Wrap around to first
  echo "${ALL_ACCOUNTS[0]}"
}

# Usage
CURRENT="cc1"
NEXT=$(get_next_account "$CURRENT")
echo "Rotating from $CURRENT to $NEXT"

Rotation with Cooldown

Prevent rotating the same agent too frequently:

ROTATION_COOLDOWN=300  # 5 minutes

should_rotate() {
  local agent_name="$1"
  local cooldown_file="/tmp/rotation-${agent_name}"

  if [ -f "$cooldown_file" ]; then
    local last_rotation=$(cat "$cooldown_file" 2>/dev/null || echo 0)
    local now=$(date +%s)
    local elapsed=$((now - last_rotation))

    if [ "$elapsed" -lt "$ROTATION_COOLDOWN" ]; then
      echo "Cooldown active (${elapsed}s / ${ROTATION_COOLDOWN}s). Skipping."
      return 1
    fi
  fi

  # Record this rotation
  date +%s > "$cooldown_file"
  return 0
}

# Usage
if should_rotate "my-agent"; then
  NEXT=$(get_next_account "$CURRENT")
  # Restart agent with new account...
fi

Full Rotation Flow (Claude Code)

rotate_claude_agent() {
  local session="$1"
  local current_account="$2"
  local next_account=$(get_next_account "$current_account")

  echo "Rotating $session: $current_account -> $next_account"

  # Gracefully exit current session
  tmux send-keys -t "$session" '/exit' Enter
  sleep 3

  # Relaunch with new account
  tmux send-keys -t "$session" \
    "CLAUDE_CONFIG_DIR=~/.claude-accounts/$next_account claude --yolo" Enter
  sleep 15

  echo "Rotated to $next_account"
}

Full Rotation Flow (Kimi Code)

rotate_kimi_agent() {
  local session="$1"
  local current_account="$2"
  local next_account=$((current_account + 1))

  # Check if next account exists, wrap around if not
  if [ ! -d ~/.kimi-accounts/$next_account ]; then
    next_account=1
  fi

  echo "Rotating $session: account $current_account -> $next_account"

  # Exit current session
  tmux send-keys -t "$session" '/exit' Enter
  sleep 3

  # Relaunch with new account
  tmux send-keys -t "$session" \
    "KIMI_SHARE_DIR=~/.kimi-accounts/$next_account kimi --yolo" Enter
  sleep 10

  echo "Rotated to account $next_account"
}

Kimi Config Fallback (API key → OAuth)

When using Kimi one-shot commands, try API key config first, fall back to OAuth:

KIMI_CONFIGS=(
  ~/.kimi-accounts/2/config.toml    # API key (try first)
  ~/.kimi-accounts/1/config.toml    # OAuth (fallback)
)
RESULT=""

for cfg in "${KIMI_CONFIGS[@]}"; do
  [ ! -f "$cfg" ] && continue
  RESULT=$(timeout 90 kimi -c "$PROMPT" --quiet --config-file "$cfg" 2>/dev/null || true)
  if [ -n "$RESULT" ] && [ ${#RESULT} -ge 10 ]; then
    break  # Got a real response
  fi
  RESULT=""
done

if [ -z "$RESULT" ]; then
  echo "All Kimi configs failed"
fi

Safety Rules

Never Kill with Ctrl-C

# NEVER DO THIS — it kills the Claude/Kimi process abruptly
tmux send-keys -t "$SESSION" C-c   # BAD! Don't do this!

# Instead, use the graceful exit command
tmux send-keys -t "$SESSION" '/exit' Enter  # Correct

Always Sanitize Captured Output

Raw tmux capture includes ANSI escapes, non-printable bytes, and potentially invalid UTF-8. Always sanitize before:

  • Passing to grep/sed
  • Feeding to another AI model
  • Storing in a file or variable for JSON serialization
sanitize_utf8() {
  LC_ALL=C sed 's/[^[:print:][:space:]]//g' | \
    iconv -f utf-8 -t utf-8 -c 2>/dev/null || cat
}

Wait Before Capturing

Never send-keys and capture-pane in the same instant. The agent needs time to process the command and render output.

# BAD — output won't have the response yet
tmux send-keys -t "$SESSION" "hello" Enter
tmux capture-pane -t "$SESSION" -p

# GOOD — give it time
tmux send-keys -t "$SESSION" "hello" Enter
sleep 5
tmux capture-pane -t "$SESSION" -p -S -50

Use Adequate Capture Depth

Shallow captures miss important context. Rate limit errors and long outputs scroll fast.

# BAD — too shallow, misses rate limit messages
tmux capture-pane -t "$SESSION" -p -S -10

# GOOD — deep enough for rate limits
tmux capture-pane -t "$SESSION" -p -S -500

Trim Before Feeding to AI

Don't feed 10,000 characters of raw tmux output to a model. Trim it:

# Trim to 2000 chars per agent
TRIMMED=$(echo "$OUTPUT" | head -c 2000)

Suppress Errors on Dead Sessions

Always redirect stderr when accessing tmux sessions that might not exist:

tmux capture-pane -t "$SESSION" -p -S -50 2>/dev/null || echo "[session not found]"
tmux has-session -t "$SESSION" 2>/dev/null
tmux send-keys -t "$SESSION" Enter 2>/dev/null || true

Quick Reference

Environment Variables

Variable Tool Purpose
CLAUDE_CONFIG_DIR Claude Code Point to account-specific config dir
KIMI_SHARE_DIR Kimi Code, OpenCode Point to account-specific share dir

Account Directories

Tool Base Dir Example
Claude Code ~/.claude-accounts/ ~/.claude-accounts/cc1/
Kimi Code ~/.kimi-accounts/ ~/.kimi-accounts/1/

Tmux Commands Cheat Sheet

# Session management
tmux new-session -d -s NAME -x 160 -y 50    # Create detached
tmux has-session -t NAME 2>/dev/null          # Check exists
tmux kill-session -t NAME 2>/dev/null         # Destroy
tmux ls                                        # List all sessions
tmux attach -t NAME                            # Attach (interactive)

# Sending input
tmux send-keys -t NAME "text" Enter            # Type + enter
tmux send-keys -t NAME Enter                   # Just press enter
tmux send-keys -t NAME Down Enter              # Navigate menu

# Capturing output
tmux capture-pane -t NAME -p                   # Capture visible
tmux capture-pane -t NAME -p -S -500           # Capture with history
tmux capture-pane -t NAME -p -S -500 | tail -100  # Last 100 lines

Timing Cheat Sheet

Operation Sleep After
Launch Claude Code 12-15s
Launch Kimi Code 5-10s
Send slash command 2-3s
Send text prompt 1-2s
/usage response 5-8s
/exit before kill 2-3s
Menu navigation 1-2s

Detection Patterns

# Rate limits
grep -qiE "rate.?limit|429|overloaded|quota.?exceeded|too many requests|credit balance is too low|insufficient_quota|hit your limit"

# Auth errors
grep -qiE "auth|unauthorized|invalid.*key|401|403|token.*expired"

# Agent idle
grep -qiE "waiting.*instruction|standing by|no work|nothing to do"

# Agent working
grep -qiE "running|executing|reading|writing|searching|thinking"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment