Skip to content

cfSandboxToSandbox().readdir splits on NUL, but Cloudflare sandbox exec strips NUL bytes from stdout #644

Description

@rp4rk

What happened

@flue/runtime@2.0.3's Cloudflare sandbox adapter, cfSandboxToSandbox().readdir in dist/cloudflare/index.mjs, lists a directory with:

async readdir(path) {
  const result = await guarded("readdir", sandbox.exec(`find ${shellQuote(path)} -mindepth 1 -maxdepth 1 -printf '%f\0'`));
  if (!result.success) throw new Error(`readdir failed for ${path}: ${result.stderr}`);
  return result.stdout.split("\0").filter((s) => s.length > 0);
}

The Cloudflare @cloudflare/sandbox Durable Object's exec strips NUL bytes from returned stdout, so a directory listing with more than one entry comes back as a single string with every filename concatenated together — there is nothing left in stdout to split on "\0".

Evidence (production, 2026-09-02, task 01M1FQ8DZ1SRDXWV6SPRA9R41M)

sandbox.exec success find '/workspace/.agents/skills' -mindepth 1 -maxdepth 1 -printf '%f\0'
sandbox.exec success stat -L -c '%s/%Y/%F' '/workspace/.agents/skills/delivery-reportconventional-commits' && ...
sandbox.exec success find '/workspace' -mindepth 1 -maxdepth 1 -printf '%f\0'
sandbox.exec success ls /workspace; ... cat /workspace/AGENTS.md.agents 2>/dev/null | head -50

/workspace/.agents/skills actually contains two directories, delivery-report and conventional-commits (see delivery/conventions.ts in our repo, which writes both). Flue's skill discovery saw one concatenated name delivery-reportconventional-commits, the follow-up stat on that name failed, and neither skill was ever loaded. Separately, the model itself ended up reading a concatenated top-level listing and guessing at a nonexistent file AGENTS.md.agents (really two entries, AGENTS.md and .agents, joined together).

Impact

Every agent using cloudflareSandbox(...) is affected, and specifically: skill auto-discovery over the Cloudflare sandbox adapter is silently broken whenever a directory being listed has more than one entry whose concatenation doesn't happen to stat successfully as one path (and even when it does, the wrong path is read).

Suggested fix

Use a delimiter that survives the sandbox's exec transport — a newline works, since find -printf '%f\n' output split on "\n" (filtering empty lines) doesn't rely on NUL bytes surviving the round trip. A filename containing a literal newline is an edge case most callers can accept losing to the same degree the existing NUL-based approach already assumed it wouldn't occur in practice.

Workaround

We've shipped a local wrapper around cloudflareSandbox(...) that overrides readdir with the newline-based version until a fixed @flue/runtime is available and pinned. See rp4rk/factori#364.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions