nanocoai/nanoclaw · warning

inbox-safety: rejecting unsafe inbox path

Error message

inbox-safety: rejecting unsafe inbox path

What it means

inbox-safety rejected an inbox path because lstat found the inbox root or per-message inbox dir is a symlink or not a directory. This is path-traversal/symlink hardening for attachment extraction: a null return means the caller refuses to write attachments.

Source

Thrown at src/inbox-safety.ts:61

 *
 * Returns the resolved, contained subdir path (write into it with an exclusive
 * flag — `COPYFILE_EXCL` / `wx` — so a pre-existing symlinked *file* can't be
 * followed either), or `null` if any guard tripped. On `null` the caller logs
 * its own context and skips; `context` is merged into the warn logs here so
 * each call site stays diagnosable.
 */
export function ensureContainedInboxDir(
  inboxRoot: string,
  messageId: string,
  context: Record<string, unknown>,
): string | null {
  const inboxDir = path.join(inboxRoot, messageId);

  for (const dir of [inboxRoot, inboxDir]) {
    try {
      const st = fs.lstatSync(dir);
      if (st.isSymbolicLink() || !st.isDirectory()) {
        log.warn('inbox-safety: rejecting unsafe inbox path', { ...context, dir });
        return null;
      }
    } catch {
      // Does not exist yet — fine, mkdir below creates it.
    }
  }

  fs.mkdirSync(inboxDir, { recursive: true });

  try {
    const realInboxDir = fs.realpathSync(inboxDir);
    const realInboxRoot = fs.realpathSync(inboxRoot);
    if (!isPathInside(realInboxRoot, realInboxDir)) {
      log.warn('inbox-safety: inbox dir escaped inbox root', { ...context, inboxDir });
      return null;
    }
    return realInboxDir;
  } catch (err) {

View on GitHub (pinned to 294ef2aee8)

Solutions

  1. ls -l the reported dir path and remove the offending symlink
  2. Recreate it as a real directory with correct ownership
  3. Audit what created the symlink (migration script, backup restore, manual tweak)
  4. Never point the inbox at another volume via symlink — mount the volume at the inbox root instead

Example fix

# before
inbox -> /mnt/bigdisk/inbox (symlink)

# after
rm inbox
mkdir inbox
# or mount /mnt/bigdisk at the inbox root in the container config
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs';
function isInboxDirSafe(p: string): boolean {
  try {
    const st = fs.lstatSync(p);
    return st.isDirectory(); // lstat: false for symlinks
  } catch { return true; } // absent is fine
}

Type guard

function isRealDirectory(p: string): boolean {
  try { return fs.lstatSync(p).isDirectory() && !fs.lstatSync(p).isSymbolicLink(); }
  catch { return false; }
}

Prevention

When it happens

Trigger: ensureContainedInboxDir lstats inboxRoot or inboxRoot/<messageId> and finds S_ISLNK or non-directory; returns null so targetInboxDir/extractAttachmentFiles skip the files.

Common situations: Someone symlinked data/.../inbox to another location (or /tmp) for debugging; a compromised or buggy writer replaced the dir; restoring from archive converted the dir to a symlink.

Related errors


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/d450f41207394513. Report an issue: GitHub.