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/4d0e343bd8e0fbeefa8c0c7e03d13b91 to your computer and use it in GitHub Desktop.

Select an option

Save thanakijwanavit/4d0e343bd8e0fbeefa8c0c7e03d13b91 to your computer and use it in GitHub Desktop.
Gas Town — Kimi Code Tools (ais CLI, kimi-account, credit checker, orchestrator skill)

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-kimi-credit.sh — Check Kimi Code status for all accounts
# Supports both API key and OAuth accounts
#
# Usage:
# bash check-kimi-credit.sh # check all accounts
# bash check-kimi-credit.sh kimi1 kimi2 # check specific accounts
#
# Setup: see README in gist
set -uo pipefail
KIMI_BIN=~/.local/bin/kimi
ACCOUNTS_DIR=~/.kimi-accounts
# If specific accounts passed as args, use those; otherwise auto-discover
if [ $# -gt 0 ]; then
ACCOUNTS=("$@")
else
ACCOUNTS=()
if [ -d "$ACCOUNTS_DIR" ]; then
for d in "$ACCOUNTS_DIR"/*/; do
[ -d "$d" ] && ACCOUNTS+=("$(basename "$d")")
done
fi
if [ ${#ACCOUNTS[@]} -eq 0 ]; then
echo ""
echo " No accounts found in $ACCOUNTS_DIR"
echo ""
echo " Quick setup:"
echo " mkdir -p ~/.kimi-accounts/main && cp -r ~/.kimi/* ~/.kimi-accounts/main/"
echo ""
exit 1
fi
fi
# ============================================================================
# Detect auth type from config.toml
# ============================================================================
detect_auth_type() {
local share_dir="$1"
local config="$share_dir/config.toml"
[ ! -f "$config" ] && echo "none" && return
# If api_key has a real value (not empty), it's API key auth
if grep -q 'api_key = "sk-' "$config" 2>/dev/null; then
echo "apikey"
elif [ -f "$share_dir/credentials/kimi-code.json" ]; then
echo "oauth"
else
echo "none"
fi
}
# ============================================================================
# Check one account
# ============================================================================
check_account() {
local acct="$1"
local share_dir="$ACCOUNTS_DIR/$acct"
[ ! -d "$share_dir" ] && echo "SKIP|$acct|–|Not found|–|–" && return
local auth_type
auth_type=$(detect_auth_type "$share_dir")
# ---------- API Key account ----------
if [ "$auth_type" = "apikey" ]; then
local key_preview
key_preview=$(grep 'api_key' "$share_dir/config.toml" 2>/dev/null | head -1 | sed 's/.*"sk-kimi-/sk-.../' | sed 's/".*//' | tail -c 9)
local api_status
local test_result
test_result=$(KIMI_SHARE_DIR="$share_dir" timeout 20 "$KIMI_BIN" -c "Say just the word 'ok'" --quiet 2>/dev/null || echo "FAILED")
if echo "$test_result" | grep -qi "ok\|yes\|hello\|sure\|certainly"; then
api_status="WORKING"
elif echo "$test_result" | grep -qi "rate.limit\|429\|quota\|exceeded\|too many"; then
api_status="RATE LIMITED"
elif echo "$test_result" | grep -qi "auth\|token\|unauthorized\|401\|403\|invalid.*key"; then
api_status="BAD KEY"
elif [ "$test_result" = "FAILED" ] || [ -z "$test_result" ]; then
api_status="NO RESPONSE"
else
api_status="WORKING"
fi
echo "OK|$acct|API key|...$key_preview|No expiry|$api_status"
return
fi
# ---------- OAuth account ----------
if [ "$auth_type" = "oauth" ]; then
local cred_file="$share_dir/credentials/kimi-code.json"
local user_id
user_id=$(python3 -c "
import json, base64
with open('$cred_file') as f:
d = json.load(f)
token = d.get('access_token', '')
if '.' in token:
payload = token.split('.')[1]
payload += '=' * (4 - len(payload) % 4)
decoded = json.loads(base64.urlsafe_b64decode(payload))
print(decoded.get('user_id', '?')[:12])
else:
print('?')
" 2>/dev/null || echo "?")
local now
now=$(date +%s)
# Refresh token expiry (this is what matters — access tokens auto-refresh)
local refresh_exp
refresh_exp=$(python3 -c "
import json, base64
with open('$cred_file') as f:
d = json.load(f)
token = d.get('refresh_token', '')
if '.' in token:
payload = token.split('.')[1]
payload += '=' * (4 - len(payload) % 4)
decoded = json.loads(base64.urlsafe_b64decode(payload))
print(decoded.get('exp', 0))
else:
print(0)
" 2>/dev/null || echo "0")
local refresh_status
if [ "$refresh_exp" -gt "$now" ] 2>/dev/null; then
local refresh_days=$(( (refresh_exp - now) / 86400 ))
refresh_status="${refresh_days}d left"
else
refresh_status="EXPIRED"
fi
# API test
local api_status
local test_result
test_result=$(KIMI_SHARE_DIR="$share_dir" timeout 20 "$KIMI_BIN" -c "Say just the word 'ok'" --quiet 2>/dev/null || echo "FAILED")
if echo "$test_result" | grep -qi "ok\|yes\|hello\|sure\|certainly"; then
api_status="WORKING"
elif echo "$test_result" | grep -qi "rate.limit\|429\|quota\|exceeded\|too many"; then
api_status="RATE LIMITED"
elif echo "$test_result" | grep -qi "auth\|token\|unauthorized\|401\|403"; then
api_status="AUTH ERROR"
elif [ "$test_result" = "FAILED" ] || [ -z "$test_result" ]; then
api_status="NO RESPONSE"
else
api_status="WORKING"
fi
echo "OK|$acct|OAuth|$user_id|$refresh_status|$api_status"
return
fi
# ---------- No auth ----------
echo "AUTH|$acct|–|No auth configured|–|–"
}
# ============================================================================
# Main
# ============================================================================
echo ""
echo " Kimi Code Account Check"
echo " ─────────────────────────────────────────────"
echo ""
declare -a RESULTS
for acct in "${ACCOUNTS[@]}"; do
printf " %-16s " "$acct"
result=$(check_account "$acct")
RESULTS+=("$result")
status=$(echo "$result" | cut -d'|' -f1)
api=$(echo "$result" | cut -d'|' -f6)
case "$status" in
OK) printf "%s\n" "$api" ;;
AUTH) printf "NOT CONFIGURED\n" ;;
SKIP) printf "NOT FOUND\n" ;;
esac
done
echo ""
echo " ┌────────────────┬──────────┬──────────────┬─────────────┬──────────────┐"
echo " │ Account │ Auth │ Identity │ Expires │ Status │"
echo " ├────────────────┼──────────┼──────────────┼─────────────┼──────────────┤"
for result in "${RESULTS[@]}"; do
status=$(echo "$result" | cut -d'|' -f1)
acct=$(echo "$result" | cut -d'|' -f2)
auth=$(echo "$result" | cut -d'|' -f3)
identity=$(echo "$result" | cut -d'|' -f4)
expires=$(echo "$result" | cut -d'|' -f5)
api=$(echo "$result" | cut -d'|' -f6)
printf " │ %-14s │ %-8s │ %-12s │ %-11s │ %-12s │\n" \
"$acct" "$auth" "$identity" "$expires" "$api"
done
echo " └────────────────┴──────────┴──────────────┴─────────────┴──────────────┘"
echo ""
echo " Run: KIMI_SHARE_DIR=~/.kimi-accounts/<name> kimi"
echo " Add: KIMI_SHARE_DIR=~/.kimi-accounts/<name> kimi login (OAuth)"
echo " or create config.toml with api_key (API key)"
echo ""
#!/bin/bash
# kimi-account — Run Kimi Code with any account by number
#
# Usage:
# kimi-account 1 # run kimi with account 1
# kimi-account 3 --yolo # run kimi account 3 in yolo mode
# kimi-account 2 -c "fix the bug" # one-shot with account 2
# kimi-account add sk-kimi-xxxxx # add a new account with API key
# kimi-account login # add a new account via OAuth (browser)
# kimi-account login 3 # re-login account 3 via OAuth
# kimi-account list # list all accounts
# kimi-account check # test all accounts
# kimi-account setup # bulk-add keys from stdin
set -uo pipefail
ACCOUNTS_DIR=~/.kimi-accounts
KIMI_BIN=~/.local/bin/kimi
OPENCODE_BIN=~/.opencode/bin/opencode
CONFIG_TEMPLATE='default_model = "kimi-code/kimi-for-coding"
default_thinking = true
default_yolo = false
[models."kimi-code/kimi-for-coding"]
provider = "kimi-api"
model = "kimi-for-coding"
max_context_size = 262144
capabilities = ["thinking", "image_in", "video_in"]
[providers.kimi-api]
type = "kimi"
base_url = "https://api.kimi.com/coding/v1"
api_key = "API_KEY_PLACEHOLDER"
[loop_control]
max_steps_per_turn = 100
max_retries_per_step = 3
reserved_context_size = 50000
[services.moonshot_search]
base_url = "https://api.kimi.com/coding/v1/search"
api_key = "API_KEY_PLACEHOLDER"
[services.moonshot_fetch]
base_url = "https://api.kimi.com/coding/v1/fetch"
api_key = "API_KEY_PLACEHOLDER"
[mcp.client]
tool_call_timeout_ms = 60000'
usage() {
echo "kimi-account — run Kimi Code with multiple accounts"
echo ""
echo " kimi-account <number> [kimi args...] Run kimi with account N"
echo " kimi-account add <api-key> Add account with API key"
echo " kimi-account login [number] Add/re-login account via OAuth"
echo " kimi-account setup Bulk-add API keys (stdin)"
echo " kimi-account list List all accounts"
echo " kimi-account check Test all accounts"
echo " kimi-account opencode <number> Run opencode with account N"
echo ""
echo "Examples:"
echo " kimi-account 1 Interactive session"
echo " kimi-account 3 --yolo Auto-approve mode"
echo " kimi-account 2 -c 'fix the tests' One-shot command"
echo " kimi-account login Add new OAuth account (next number)"
echo " kimi-account login 3 Re-login account 3 via browser"
echo " kimi-account opencode 1 Run opencode with account 1"
echo ""
}
# ── add: create account from API key ──
cmd_add() {
local key="$1"
if [[ ! "$key" == sk-kimi-* ]] && [[ ! "$key" == sk-* ]]; then
echo " Error: key should start with sk-kimi- or sk-"
return 1
fi
# Find next available number
local n=1
while [ -d "$ACCOUNTS_DIR/$n" ]; do
n=$((n + 1))
done
mkdir -p "$ACCOUNTS_DIR/$n"
echo "$CONFIG_TEMPLATE" | sed "s|API_KEY_PLACEHOLDER|$key|g" > "$ACCOUNTS_DIR/$n/config.toml"
echo " Added account $n (key: ...${key: -8})"
}
# ── login: add/re-login via OAuth ──
cmd_login() {
local n="${1:-}"
if [ -z "$n" ]; then
# Find next available number
n=1
while [ -d "$ACCOUNTS_DIR/$n" ]; do
n=$((n + 1))
done
echo " Creating account $n (OAuth)..."
else
echo " Logging in account $n (OAuth)..."
fi
mkdir -p "$ACCOUNTS_DIR/$n"
# Write OAuth config if no config exists yet
if [ ! -f "$ACCOUNTS_DIR/$n/config.toml" ]; then
cat > "$ACCOUNTS_DIR/$n/config.toml" << 'OAUTH_CFG'
default_model = "kimi-code/kimi-for-coding"
default_thinking = true
default_yolo = false
[models."kimi-code/kimi-for-coding"]
provider = "managed:kimi-code"
model = "kimi-for-coding"
max_context_size = 262144
capabilities = ["thinking", "image_in", "video_in"]
[providers."managed:kimi-code"]
type = "kimi"
base_url = "https://api.kimi.com/coding/v1"
api_key = ""
[providers."managed:kimi-code".oauth]
storage = "file"
key = "oauth/kimi-code"
[loop_control]
max_steps_per_turn = 100
max_retries_per_step = 3
max_ralph_iterations = 0
reserved_context_size = 50000
[services.moonshot_search]
base_url = "https://api.kimi.com/coding/v1/search"
api_key = ""
[services.moonshot_search.oauth]
storage = "file"
key = "oauth/kimi-code"
[services.moonshot_fetch]
base_url = "https://api.kimi.com/coding/v1/fetch"
api_key = ""
[services.moonshot_fetch.oauth]
storage = "file"
key = "oauth/kimi-code"
[mcp.client]
tool_call_timeout_ms = 60000
OAUTH_CFG
fi
echo " Open the URL below in your browser and log in."
echo ""
KIMI_SHARE_DIR="$ACCOUNTS_DIR/$n" "$KIMI_BIN" login
local exit_code=$?
if [ $exit_code -eq 0 ] && [ -f "$ACCOUNTS_DIR/$n/credentials/kimi-code.json" ]; then
echo ""
echo " Account $n logged in successfully."
echo " Run: kimi-account $n"
else
echo ""
echo " Login may have failed. Try again: kimi-account login $n"
fi
}
# ── setup: bulk-add keys ──
cmd_setup() {
echo " Paste API keys, one per line. Press Ctrl+D when done."
echo " (Lines that don't start with sk- are skipped)"
echo ""
local count=0
while IFS= read -r line; do
line=$(echo "$line" | xargs) # trim whitespace
[ -z "$line" ] && continue
[[ ! "$line" == sk-* ]] && continue
cmd_add "$line"
count=$((count + 1))
done
echo ""
echo " Added $count accounts. Total: $(ls -d "$ACCOUNTS_DIR"/*/ 2>/dev/null | wc -l)"
}
# ── list: show all accounts ──
cmd_list() {
echo ""
echo " Kimi Accounts ($ACCOUNTS_DIR)"
echo " ───────────────────────────────────────"
if [ ! -d "$ACCOUNTS_DIR" ] || [ -z "$(ls -A "$ACCOUNTS_DIR" 2>/dev/null)" ]; then
echo " (none — run 'kimi-account add <key>' to add)"
echo ""
return
fi
for d in "$ACCOUNTS_DIR"/*/; do
[ ! -d "$d" ] && continue
local name
name=$(basename "$d")
local auth="?"
local key_preview=""
if grep -q 'api_key = "sk-' "$d/config.toml" 2>/dev/null; then
auth="API key"
key_preview=$(grep 'api_key' "$d/config.toml" 2>/dev/null | head -1 | grep -o 'sk-[^"]*' | head -1)
key_preview="...${key_preview: -8}"
elif [ -f "$d/credentials/kimi-code.json" ]; then
auth="OAuth"
key_preview="(browser login)"
else
auth="—"
key_preview="not configured"
fi
printf " %-4s %-8s %s\n" "$name" "$auth" "$key_preview"
done
echo ""
echo " Run: kimi-account <number> [args...]"
echo ""
}
# ── check: test all accounts ──
cmd_check() {
echo ""
echo " Testing all accounts..."
echo ""
for d in "$ACCOUNTS_DIR"/*/; do
[ ! -d "$d" ] && continue
local name
name=$(basename "$d")
printf " %-4s " "$name"
local result
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\|sure\|certainly"; then
echo "WORKING"
elif echo "$result" | grep -qi "rate.limit\|429\|quota\|exceeded"; then
echo "RATE LIMITED"
elif echo "$result" | grep -qi "auth\|unauthorized\|invalid.*key\|401\|403"; then
echo "BAD KEY"
elif [ "$result" = "FAILED" ] || [ -z "$result" ]; then
echo "NO RESPONSE"
else
echo "WORKING"
fi
done
echo ""
}
# ── run: launch kimi with account ──
cmd_run() {
local acct="$1"
shift
local share_dir="$ACCOUNTS_DIR/$acct"
if [ ! -d "$share_dir" ]; then
echo " Account '$acct' not found."
echo " Run 'kimi-account list' to see available accounts."
exit 1
fi
exec env KIMI_SHARE_DIR="$share_dir" "$KIMI_BIN" "$@"
}
# ── opencode: launch opencode with account ──
cmd_opencode() {
local acct="$1"
shift
local share_dir="$ACCOUNTS_DIR/$acct"
if [ ! -d "$share_dir" ]; then
echo " Account '$acct' not found."
exit 1
fi
exec env KIMI_SHARE_DIR="$share_dir" "$OPENCODE_BIN" "$@"
}
# ============================================================================
# Main
# ============================================================================
mkdir -p "$ACCOUNTS_DIR"
if [ $# -eq 0 ]; then
usage
exit 0
fi
case "$1" in
-h|--help|help)
usage ;;
add)
[ $# -lt 2 ] && echo " Usage: kimi-account add <api-key>" && exit 1
cmd_add "$2" ;;
setup)
cmd_setup ;;
list|ls)
cmd_list ;;
check|test)
cmd_check ;;
login)
shift
cmd_login "$@" ;;
opencode|oc)
[ $# -lt 2 ] && echo " Usage: kimi-account opencode <number>" && exit 1
shift
cmd_opencode "$@" ;;
[0-9]*)
cmd_run "$@" ;;
*)
# Try as account name
if [ -d "$ACCOUNTS_DIR/$1" ]; then
cmd_run "$@"
else
echo " Unknown command: $1"
usage
exit 1
fi ;;
esac

Kimi Code Multi-Account Setup

Run multiple Kimi accounts for AI agents, rate-limit rotation, or team use.

Quick Start with kimi-account wrapper

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

# Add accounts
kimi-account add sk-kimi-YOUR_API_KEY     # API key method
kimi-account login                         # OAuth method (browser)

# Run
kimi-account 1                             # interactive session
kimi-account 2 --yolo                      # auto-approve mode
kimi-account 1 -c "fix the tests"          # one-shot command
kimi-account opencode 1                    # run opencode with account 1

# Manage
kimi-account list                          # list all accounts
kimi-account check                         # test all accounts
kimi-account setup                         # bulk-add keys (paste, Ctrl+D)
kimi-account login 3                       # re-login account 3 via OAuth

The Core Concept

KIMI_SHARE_DIR=~/.kimi-accounts/<number> kimi

This env var tells Kimi where to store credentials, sessions, and config. Each account gets its own folder. Fully isolated. The kimi-account wrapper manages this for you.


Adding Accounts

API Key (no browser needed — best for servers)

kimi-account add sk-kimi-YOUR_KEY_HERE

Or manually create the config:

mkdir -p ~/.kimi-accounts/2
cat > ~/.kimi-accounts/2/config.toml << 'EOF'
default_model = "kimi-code/kimi-for-coding"
default_thinking = true
default_yolo = false

[models."kimi-code/kimi-for-coding"]
provider = "kimi-api"
model = "kimi-for-coding"
max_context_size = 262144
capabilities = ["thinking", "image_in", "video_in"]

[providers.kimi-api]
type = "kimi"
base_url = "https://api.kimi.com/coding/v1"
api_key = "sk-kimi-PASTE_YOUR_KEY_HERE"

[loop_control]
max_steps_per_turn = 100
max_retries_per_step = 3
reserved_context_size = 50000

[services.moonshot_search]
base_url = "https://api.kimi.com/coding/v1/search"
api_key = "sk-kimi-PASTE_YOUR_KEY_HERE"

[services.moonshot_fetch]
base_url = "https://api.kimi.com/coding/v1/fetch"
api_key = "sk-kimi-PASTE_YOUR_KEY_HERE"

[mcp.client]
tool_call_timeout_ms = 60000
EOF

Replace sk-kimi-PASTE_YOUR_KEY_HERE in all 3 places. Get keys from platform.kimi.com.

OAuth (browser login)

kimi-account login          # creates next numbered account
kimi-account login 3        # re-login specific account

Or manually:

KIMI_SHARE_DIR=~/.kimi-accounts/3 kimi login
# Prints a URL → open in browser → log in → done

Bulk Setup (10+ keys)

kimi-account setup
# Paste keys one per line, Ctrl+D when done

Save your existing account

mkdir -p ~/.kimi-accounts/1
cp -r ~/.kimi/* ~/.kimi-accounts/1/

Running with Different Tools

Kimi CLI

kimi-account 1                              # interactive
kimi-account 1 -c "fix the bug" --quiet     # one-shot
kimi-account 1 --yolo                       # auto-approve

OpenCode (uses Kimi under the hood)

kimi-account opencode 1

In tmux (background agents)

tmux new-session -d -s my-agent
tmux send-keys -t my-agent 'kimi-account 2 --yolo' Enter

With Gas Town (gt)

KIMI_SHARE_DIR=~/.kimi-accounts/1 gt deacon start --agent opencode
KIMI_SHARE_DIR=~/.kimi-accounts/2 gt crew start dev --agent opencode

Check All Accounts

kimi-account check
# or the detailed version:
bash check-kimi-credit.sh
  ┌────────────────┬──────────┬──────────────┬─────────────┬──────────────┐
  │ Account        │ Auth     │ Identity     │ Expires     │ Status       │
  ├────────────────┼──────────┼──────────────┼─────────────┼──────────────┤
  │ 1              │ OAuth    │ d6cmrm7ftae6 │ 29d left    │ WORKING      │
  │ 2              │ API key  │ ...xj8cDDPw  │ No expiry   │ WORKING      │
  └────────────────┴──────────┴──────────────┴─────────────┴──────────────┘

API Key vs OAuth

API Key OAuth
Setup kimi-account add <key> kimi-account login
Headless Works perfectly Needs browser for initial login
Expiry Never (key is permanent) Refresh token lasts ~30 days
Best for Servers, CI/CD, agents Personal dev, interactive

File Structure

~/.kimi-accounts/
├── 1/                    # Account 1
│   ├── config.toml       # OAuth or API key config
│   ├── credentials/      # OAuth tokens (if OAuth)
│   └── sessions/
├── 2/                    # Account 2
│   ├── config.toml       # API key config (api_key = "sk-kimi-...")
│   └── sessions/
└── ...

Env Vars Reference

Variable Tool Purpose
KIMI_SHARE_DIR kimi, opencode Override ~/.kimi — the main switch for multi-account
CLAUDE_CONFIG_DIR claude code Same concept for Claude Code accounts

Notes

  • Kimi Code is free — no credit purchase needed, but has rate limits
  • OAuth access tokens expire in ~15 min but auto-refresh silently
  • OAuth refresh tokens last ~30 days then need kimi-account login N again
  • API keys never expire — simpler for long-running agents
  • KIMI_SHARE_DIR was found in kimi-cli source at kimi_cli/share.py
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