shareAI-lab/learn-claude-code · error · ValueError

Mailbox path escapes directory: {agent!r}

Error message

Mailbox path escapes directory: {agent!r}

What it means

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.

Source

Thrown at s13_agent_teams/code.py:795


def is_valid_agent_name(name: str) -> bool:
    return bool(VALID_AGENT_NAME.fullmatch(name))


class MessageBus:
    """Thread-safe file mailboxes with destructive reads."""

    def __init__(self):
        self._lock = threading.RLock()
        self._changed = threading.Condition(self._lock)

    def _path(self, agent: str) -> Path:
        if not is_valid_agent_name(agent):
            raise ValueError(f"Invalid mailbox recipient: {agent!r}")
        path = (MAILBOX_DIR / f"{agent}.jsonl").resolve()
        if not path.is_relative_to(MAILBOX_ROOT):
            raise ValueError(f"Mailbox path escapes directory: {agent!r}")
        return path

    def _read_unlocked(self, agent: str) -> list[dict]:
        inbox = self._path(agent)
        if not inbox.exists():
            return []
        msgs = [json.loads(line) for line in inbox.read_text().splitlines()
                if line.strip()]
        inbox.unlink()
        return msgs

    def send(self, from_agent: str, to_agent: str, content: str,
             msg_type: str = "message", metadata: dict | None = None):
        msg = {"from": from_agent, "to": to_agent,
               "content": content, "type": msg_type,
               "ts": time.time(), "metadata": metadata or {}}
        with self._changed:
            MAILBOX_DIR.mkdir(parents=True, exist_ok=True)

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Make .mailboxes a real directory inside the workspace.
  2. Resolve WORKDIR at startup and keep it stable for the process lifetime.
  3. In tests, recreate the module after changing workspace paths.

Example fix

# before
ln -s /mnt/shared/mail .mailboxes

# after
rm .mailboxes && mkdir .mailboxes
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def mailbox_root_inside_workdir() -> bool:
    return MAILBOX_ROOT.is_relative_to(WORKDIR.resolve()) and not MAILBOX_DIR.is_symlink()

Try / catch

try:
    bus.send(src, dst, content)
except ValueError as exc:
    if 'escapes directory' in str(exc):
        logging.exception('mailbox layout unsafe; check %s for symlinks', MAILBOX_DIR)
    raise

Prevention

When it happens

Trigger: .mailboxes is a symlink to a directory outside WORKDIR; workspace symlink retargeted between import and message send; tests monkeypatching WORKDIR without recomputing MAILBOX_ROOT.

Common situations: Putting mailboxes on shared storage via symlink; macOS /tmp resolution; test fixture global mutation.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/b7f7b1702a39d6e9. Report an issue: GitHub.