{"record":{"id":"3b6b45a8f925d4a6","repo":"odysseus-dev/odysseus","slug":"tmux-session-required-a-za-z0-9-only","errorCode":null,"errorMessage":"tmux_session required, [a-zA-Z0-9_-]+ only","messagePattern":"tmux_session required, \\[a-zA-Z0-9_-\\]\\+ only","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/codex_routes.py","lineNumber":839,"sourceCode":"        if serve_endpoint is None:\n            raise HTTPException(503, \"model serve endpoint unavailable\")\n        return await serve_endpoint(request, req)\n\n    @router.post(\"/cookbook/adopt\")\n    async def codex_cookbook_adopt(request: Request, body: dict[str, Any] = Body(default_factory=dict)):\n        \"\"\"Adopt an existing tmux session (one started via raw ssh+tmux) into\n        cookbook tracking. Needed when serve_model rejects a cmd and the\n        agent falls back to direct ssh — without adoption the session is\n        invisible to the UI. Body: {tmux_session, model, host?, port?}.\"\"\"\n        _require_cookbook_scope(request, COOKBOOK_LAUNCH_SCOPES)\n        norm = dict(body or {})\n        sess = (norm.get(\"tmux_session\") or norm.get(\"session_id\") or \"\").strip()\n        model = (norm.get(\"model\") or norm.get(\"repo_id\") or \"\").strip()\n        host = validate_remote_host((norm.get(\"host\") or norm.get(\"remote_host\") or \"\").strip() or None) or \"\"\n        port = norm.get(\"port\") or 8000\n        import re as _re\n        if not sess or not _re.fullmatch(r\"[a-zA-Z0-9_-]+\", sess):\n            raise HTTPException(400, \"tmux_session required, [a-zA-Z0-9_-]+ only\")\n        if not model:\n            raise HTTPException(400, \"model required\")\n        # Verify the tmux session exists on the target host before adopting.\n        import shlex\n        if host:\n            check = f\"ssh {shlex.quote(host)} 'tmux has-session -t {shlex.quote(sess)}'\"\n        else:\n            check = f\"tmux has-session -t {shlex.quote(sess)}\"\n        chk = await _run_shell(check, timeout=8)\n        if chk.get(\"exit_code\") not in (0, None):\n            raise HTTPException(404, f\"tmux session {sess!r} not found on {host or 'local'}\")\n        # Write into cookbook_state.json.\n        import time as _t, json as _json\n        from core.atomic_io import atomic_write_json\n        from pathlib import Path as _Path\n        cookbook_state_path = _Path(COOKBOOK_STATE_FILE)\n        try:\n            state = _json.loads(cookbook_state_path.read_text(encoding=\"utf-8\"))","sourceCodeStart":821,"sourceCodeEnd":857,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/codex_routes.py#L821-L857","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\ntmux new-session -d -s 'vllm.server'   # dot not allowed by adopt\n\n# after\ntmux new-session -d -s 'vllm-server'\n# then POST /cookbook/adopt {\"tmux_session\":\"vllm-server\", ...}","handlingStrategy":"validation","validationCode":"import re\nassert re.fullmatch(r'[a-zA-Z0-9_-]+', session), 'rename tmux session first'","typeGuard":"const isAdoptableSession = (s: string) => /^[a-zA-Z0-9_-]+$/.test(s)","tryCatchPattern":null,"preventionTips":["Create cookbook-tracked tmux sessions with slug names from the start.","Check `tmux ls` names for dots/colons before adopting."],"tags":["tmux","input-validation","shell-safety","http-400"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}