A session manager for running multiple Claude Code instances with real-time streaming, WebSocket API, and terminal UI.
- Multi-session management - Run multiple Claude Code instances in parallel
- Real-time streaming - Live terminal output via WebSocket (no polling)
- Terminal UI - Navigate sessions with vim-like keybindings
- WebSocket API - Send input, receive streams, manage sessions
- GNU Screen integration - Reliable session persistence
- Python 3.11+
- GNU Screen
- Claude Code CLI (
claudecommand available in PATH)
# Verify prerequisites
python3 --version # 3.11+
screen --version # GNU Screen
which claude # Claude Code CLIgit clone https://github.com/Nominate-AI/cbos.git
cd cbos
# Create/activate virtual environment (recommended)
python3 -m venv .venv
source .venv/bin/activate
# Install
pip install -e .mkdir -p ~/claude_streams ~/claude_logsCBOS can be configured via environment variables or a ~/.cbos/.env file.
# Copy example configuration
cp .env.example ~/.cbos/.env
# Edit configuration
vim ~/.cbos/.envKey configuration options:
# Claude command path (default: "claude")
# Use full path if Claude is not in your PATH
CBOS_CLAUDE_COMMAND=/home/user/.local/bin/claude
# Environment variables to pass to Claude
# Example: Set thinking token limit
CBOS_CLAUDE_ENV_VARS=MAX_THINKING_TOKENS=32000
# API settings
CBOS_API_HOST=127.0.0.1
CBOS_API_PORT=32205
# Stream directory for typescript files
CBOS_STREAM_STREAM_DIR=/home/user/claude_streams# Copy service file
sudo cp systemd/cbos.service /etc/systemd/system/
# Edit paths if needed (default assumes ~/.pyenv/versions/nominates)
sudo vim /etc/systemd/system/cbos.service
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable cbos
sudo systemctl start cbos
# Verify
systemctl status cbos# Terminal 1: Start API server
cbos-api
# Terminal 2: Launch TUI
cbos# Start service
sudo systemctl start cbos
# Launch TUI (connects to running service)
cbosSessions are created through the CBOS API - you don't start screen sessions manually.
# Create a new session
curl -X POST http://localhost:32205/sessions \
-H "Content-Type: application/json" \
-d '{"slug": "MYPROJECT", "path": "/home/user/myproject"}'CBOS automatically:
- Creates a GNU Screen session with the specified name
- Wraps Claude in
script -ffor real-time streaming capture - Sets
NO_COLOR=1for cleaner terminal output - Starts Claude Code in the specified working directory
What runs under the hood:
# With default configuration (CBOS_CLAUDE_COMMAND=claude)
screen -dmS MYPROJECT -L -Logfile ~/claude_logs/MYPROJECT.log bash -c \
"script -f --timing=~/claude_streams/MYPROJECT.timing \
~/claude_streams/MYPROJECT.typescript \
-c 'cd /home/user/myproject && NO_COLOR=1 claude'"
# With custom configuration (e.g., CBOS_CLAUDE_ENV_VARS=MAX_THINKING_TOKENS=32000)
screen -dmS MYPROJECT -L -Logfile ~/claude_logs/MYPROJECT.log bash -c \
"script -f --timing=~/claude_streams/MYPROJECT.timing \
~/claude_streams/MYPROJECT.typescript \
-c 'cd /home/user/myproject && MAX_THINKING_TOKENS=32000 NO_COLOR=1 /path/to/claude'"Note: Existing screen sessions started manually (not through CBOS) will appear in the list but won't stream - they weren't wrapped with script -f. Kill and recreate them through CBOS to enable streaming.
To attach directly (bypass CBOS TUI):
screen -r MYPROJECT┌─────────────────────────────────────────────────────────────┐
│ Sessions │ Buffer Content │
│ ───────── │ ───────────── │
│ ● AUTH │ > What should I do next? │
│ ○ INTEL │ │
│ ◐ DOCS │ ● Thinking about your request... │
│ ○ APP │ │
├───────────────┴─────────────────────────────────────────────┤
│ ● streaming │ CBOS v0.7.0 │
└─────────────────────────────────────────────────────────────┘
| Key | Action |
|---|---|
c |
Create new session (project picker) |
j/k |
Navigate sessions |
Enter |
Focus input field |
Escape |
Back to session list |
Ctrl+C |
Send interrupt to session |
r |
Reconnect WebSocket |
a |
Show attach command |
s |
Get AI suggestion |
q |
Quit |
When you press c, CBOS discovers all Claude projects by finding CLAUDE.md files:
| Key | Action |
|---|---|
j/k |
Navigate projects |
Enter |
Create session for selected project |
n/p |
Next/previous page |
Escape |
Cancel |
Session names are auto-generated from the git remote origin URL (e.g., github.com/user/myrepo.git → MYREPO).
| Icon | State | Meaning |
|---|---|---|
● |
waiting | Prompt visible, awaiting input |
◐ |
thinking | Claude is processing |
◑ |
working | Executing tools |
○ |
idle | No activity detected |
# List sessions
curl http://localhost:32205/sessions
# Create session
curl -X POST http://localhost:32205/sessions \
-H "Content-Type: application/json" \
-d '{"slug": "MYPROJECT", "path": "/path/to/project"}'
# Send input
curl -X POST http://localhost:32205/sessions/MYPROJECT/send \
-H "Content-Type: application/json" \
-d '{"text": "Hello Claude"}'
# Send interrupt (Ctrl+C)
curl -X POST http://localhost:32205/sessions/MYPROJECT/interrupt
# Get buffer
curl http://localhost:32205/sessions/MYPROJECT/buffer
# Kill session
curl -X DELETE http://localhost:32205/sessions/MYPROJECTConnect to ws://localhost:32205/ws/stream for real-time updates.
import asyncio
import websockets
import json
async def stream():
async with websockets.connect("ws://localhost:32205/ws/stream") as ws:
# Subscribe to all sessions
await ws.send(json.dumps({
"type": "subscribe",
"sessions": ["*"] # or ["AUTH", "INTEL"]
}))
# Receive stream events
async for message in ws:
data = json.loads(message)
if data["type"] == "stream":
print(f"[{data['session']}] {data['data']}")
asyncio.run(stream())Client → Server:
{"type": "subscribe", "sessions": ["*"]}
{"type": "send", "session": "AUTH", "text": "yes"}
{"type": "interrupt", "session": "AUTH"}Server → Client:
{"type": "sessions", "sessions": [...]}
{"type": "stream", "session": "AUTH", "data": "...", "ts": 1704326400.123}
{"type": "subscribed", "sessions": ["AUTH"]}┌──────────────────────────────────────────────────────────────┐
│ TUI (cbos) │
│ WebSocket Client │
└─────────────────────────────┬────────────────────────────────┘
│ ws://localhost:32205/ws/stream
▼
┌──────────────────────────────────────────────────────────────┐
│ CBOS API Server │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ REST API │ │ WebSocket │ │ StreamManager │ │
│ │ /sessions │ │ /ws/stream │ │ (watchfiles) │ │
│ └─────────────┘ └──────────────┘ └────────┬─────────┘ │
└──────────────────────────────────────────────┼───────────────┘
│ watches
▼
┌──────────────────────────────────────────────────────────────┐
│ ~/claude_streams/ │
│ AUTH.typescript INTEL.typescript DOCS.typescript ... │
└──────────────────────────────────────────────────────────────┘
▲ script -f writes
│
┌──────────────────────────────────────────────────────────────┐
│ GNU Screen Sessions │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ AUTH │ │ INTEL │ │ DOCS │ ... │
│ │ (claude) │ │ (claude) │ │ (claude) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────────────────────────────────────────────────┘
| Variable | Default | Description |
|---|---|---|
CBOS_LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING, ERROR) |
CBOS_STREAM_DIR |
~/claude_streams |
Directory for typescript files |
The service file at /etc/systemd/system/cbos.service:
[Unit]
Description=CBOS - Claude Code Session Manager API
After=network.target
[Service]
Type=simple
User=yourusername
Group=yourusername
WorkingDirectory=/path/to/cbos
Environment="PATH=/home/user/.local/bin:/usr/local/bin:/usr/bin"
Environment="CBOS_LOG_LEVEL=INFO"
ExecStart=/path/to/venv/bin/uvicorn cbos.api.main:app --host 127.0.0.1 --port 32205
Restart=always
RestartSec=5
SyslogIdentifier=cbos
StandardOutput=journal
StandardError=journal
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target# Install with dev dependencies
pip install -e ".[dev]"
# Run tests
pytest tests/ -v
# Run API server in development
uvicorn cbos.api.main:app --reload --port 32205
# View logs
sudo journalctl -u cbos -fCheck if the session was created with streaming enabled:
# Session should show script wrapper in process tree
pstree -a $(pgrep -f "SCREEN.*MYSESSION")Older sessions (created before streaming) need to be restarted.
# Check if API is running
curl http://localhost:32205/sessions
# Check service status
systemctl status cbosInput requires carriage return. If using the API directly:
# Use the /send endpoint (handles CR automatically)
curl -X POST http://localhost:32205/sessions/AUTH/send \
-H "Content-Type: application/json" \
-d '{"text": "your input here"}'MIT