{"record":{"id":"b7f7b1702a39d6e9","repo":"shareAI-lab/learn-claude-code","slug":"mailbox-path-escapes-directory-agent-r","errorCode":null,"errorMessage":"Mailbox path escapes directory: {agent!r}","messagePattern":"Mailbox path escapes directory: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"s13_agent_teams/code.py","lineNumber":795,"sourceCode":"\n\ndef is_valid_agent_name(name: str) -> bool:\n    return bool(VALID_AGENT_NAME.fullmatch(name))\n\n\nclass MessageBus:\n    \"\"\"Thread-safe file mailboxes with destructive reads.\"\"\"\n\n    def __init__(self):\n        self._lock = threading.RLock()\n        self._changed = threading.Condition(self._lock)\n\n    def _path(self, agent: str) -> Path:\n        if not is_valid_agent_name(agent):\n            raise ValueError(f\"Invalid mailbox recipient: {agent!r}\")\n        path = (MAILBOX_DIR / f\"{agent}.jsonl\").resolve()\n        if not path.is_relative_to(MAILBOX_ROOT):\n            raise ValueError(f\"Mailbox path escapes directory: {agent!r}\")\n        return path\n\n    def _read_unlocked(self, agent: str) -> list[dict]:\n        inbox = self._path(agent)\n        if not inbox.exists():\n            return []\n        msgs = [json.loads(line) for line in inbox.read_text().splitlines()\n                if line.strip()]\n        inbox.unlink()\n        return msgs\n\n    def send(self, from_agent: str, to_agent: str, content: str,\n             msg_type: str = \"message\", metadata: dict | None = None):\n        msg = {\"from\": from_agent, \"to\": to_agent,\n               \"content\": content, \"type\": msg_type,\n               \"ts\": time.time(), \"metadata\": metadata or {}}\n        with self._changed:\n            MAILBOX_DIR.mkdir(parents=True, exist_ok=True)","sourceCodeStart":777,"sourceCodeEnd":813,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s13_agent_teams/code.py#L777-L813","documentation":"The second check in MessageBus._path(): after the name passes the alphabet regex, the resolved .mailboxes/<agent>.jsonl path must still be inside MAILBOX_ROOT. Since the regex already forbids '/' and '.', this branch fires only when the environment shifts under the process — .mailboxes is a symlink, or MAILBOX_ROOT (resolved at import) no longer sits under the current WORKDIR because the workspace symlink changed.","triggerScenarios":".mailboxes is a symlink to a directory outside WORKDIR; workspace symlink retargeted between import and message send; tests monkeypatching WORKDIR without recomputing MAILBOX_ROOT.","commonSituations":"Putting mailboxes on shared storage via symlink; macOS /tmp resolution; test fixture global mutation.","solutions":["Make .mailboxes a real directory inside the workspace.","Resolve WORKDIR at startup and keep it stable for the process lifetime.","In tests, recreate the module after changing workspace paths."],"exampleFix":"# before\nln -s /mnt/shared/mail .mailboxes\n\n# after\nrm .mailboxes && mkdir .mailboxes","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef mailbox_root_inside_workdir() -> bool:\n    return MAILBOX_ROOT.is_relative_to(WORKDIR.resolve()) and not MAILBOX_DIR.is_symlink()","typeGuard":null,"tryCatchPattern":"try:\n    bus.send(src, dst, content)\nexcept ValueError as exc:\n    if 'escapes directory' in str(exc):\n        logging.exception('mailbox layout unsafe; check %s for symlinks', MAILBOX_DIR)\n    raise","preventionTips":["Keep .mailboxes a real directory inside the workspace.","Resolve WORKDIR at startup; don't rotate workspace symlinks mid-run.","Add a startup assertion MAILBOX_ROOT.is_relative_to(WORKDIR.resolve())."],"tags":["path-traversal","symlink","message-bus","workspace"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}