odysseus-dev/odysseus · warning · HTTPException
tmux_session required, [a-zA-Z0-9_-]+ only
Error message
tmux_session required, [a-zA-Z0-9_-]+ only
What it means
Raised by POST /api/codex/cookbook/adopt when the tmux_session field is missing or contains characters outside [a-zA-Z0-9_-]. The session name is later interpolated into a shell command (tmux has-session, locally or via ssh), so this validation is a command-injection guard and is strict by design.
Source
Thrown at routes/codex_routes.py:839
if serve_endpoint is None:
raise HTTPException(503, "model serve endpoint unavailable")
return await serve_endpoint(request, req)
@router.post("/cookbook/adopt")
async def codex_cookbook_adopt(request: Request, body: dict[str, Any] = Body(default_factory=dict)):
"""Adopt an existing tmux session (one started via raw ssh+tmux) into
cookbook tracking. Needed when serve_model rejects a cmd and the
agent falls back to direct ssh — without adoption the session is
invisible to the UI. Body: {tmux_session, model, host?, port?}."""
_require_cookbook_scope(request, COOKBOOK_LAUNCH_SCOPES)
norm = dict(body or {})
sess = (norm.get("tmux_session") or norm.get("session_id") or "").strip()
model = (norm.get("model") or norm.get("repo_id") or "").strip()
host = validate_remote_host((norm.get("host") or norm.get("remote_host") or "").strip() or None) or ""
port = norm.get("port") or 8000
import re as _re
if not sess or not _re.fullmatch(r"[a-zA-Z0-9_-]+", sess):
raise HTTPException(400, "tmux_session required, [a-zA-Z0-9_-]+ only")
if not model:
raise HTTPException(400, "model required")
# Verify the tmux session exists on the target host before adopting.
import shlex
if host:
check = f"ssh {shlex.quote(host)} 'tmux has-session -t {shlex.quote(sess)}'"
else:
check = f"tmux has-session -t {shlex.quote(sess)}"
chk = await _run_shell(check, timeout=8)
if chk.get("exit_code") not in (0, None):
raise HTTPException(404, f"tmux session {sess!r} not found on {host or 'local'}")
# Write into cookbook_state.json.
import time as _t, json as _json
from core.atomic_io import atomic_write_json
from pathlib import Path as _Path
cookbook_state_path = _Path(COOKBOOK_STATE_FILE)
try:
state = _json.loads(cookbook_state_path.read_text(encoding="utf-8"))View on GitHub (pinned to f9235ebbf1)
Solutions
- Rename the tmux session before adopting: tmux rename-session (or start with) a name using only [a-zA-Z0-9_-].
- Send the session name exactly as allowed; strip disallowed characters client-side only if the actual session name matches.
- If the session has a dot, create a new session with a safe name running the same command.
Example fix
# before
tmux new-session -d -s 'vllm.server' # dot not allowed by adopt
# after
tmux new-session -d -s 'vllm-server'
# then POST /cookbook/adopt {"tmux_session":"vllm-server", ...} Defensive patterns
Strategy: validation
Validate before calling
import re assert re.fullmatch(r'[a-zA-Z0-9_-]+', session), 'rename tmux session first'
Type guard
const isAdoptableSession = (s: string) => /^[a-zA-Z0-9_-]+$/.test(s)
Prevention
- Create cookbook-tracked tmux sessions with slug names from the start.
- Check `tmux ls` names for dots/colons before adopting.
When it happens
Trigger: Calling /cookbook/adopt with tmux_session/session_id empty, containing a dot, colon, space, or shell metacharacters. Note: tmux itself permits dots and colons in session names, but this API rejects them.
Common situations: Default tmux session names with dots (e.g. 'my.session') created outside the cookbook; client sends session_id instead of a sanitized value; name copied from 'tmux ls' output containing a dot or colon.
Related errors
- await res.text()
- HTTP ${res.status}
- data.error || ('HTTP ' + (data.status || 500))
- model returned no rewritten text
- Current password is incorrect
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/3b6b45a8f925d4a6.
Report an issue: GitHub.