Skip to content

feat(cli): enforce /add-dir via sandbox writable roots — in every host - #214

Merged
oratis merged 1 commit into
mainfrom
feat/add-dir-enforce
Aug 2, 2026
Merged

feat(cli): enforce /add-dir via sandbox writable roots — in every host#214
oratis merged 1 commit into
mainfrom
feat/add-dir-enforce

Conversation

@oratis

@oratis oratis commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Reworks #166. Same core idea, but that PR is now both incomplete and carries stale docs — details below.

The gap

permissions.additionalDirectories is declared in the settings schema and read by /permissions for display, but consumed for enforcement nowhere. /add-dir printed:

Recorded /some/path as additional allowed directory (effective in M3).

M3 shipped. Nothing ever started reading it. A user running /add-dir would reasonably believe it took effect.

Why the sandbox is the right boundary

The file tools (Read/Write/Edit/Glob/Grep) already accept any absolute path — there is no cwd containment to widen, so there's nothing for /add-dir to "unlock" there. The only thing that actually restricts writes is the OS sandbox wrapping Bash. So enforcing /add-dir means folding those directories into filesystem.allowWrite.

That's #166's insight and it's correct.

What #166 could not have covered

#166 was written before the app-server existed, and wires only the CLI REPL and headless paths. Since #192/#195/#196, desktop, VS Code, and LSP all run through apps/server. Merging #166 as-is would enforce the setting in the CLI and silently ignore it in every other client.

For a security-relevant setting that's worse than not shipping it, because the two states are indistinguishable from the UI — /add-dir reports success either way.

All 7 sandbox assembly sites across 4 files now route through one helper:

file sites
apps/cli/src/repl.ts 2 (RuntimeHost + plugin capability bridge)
apps/cli/src/headless.ts 2 (same pair)
apps/server/src/default-runtime.ts 1 (RuntimeHost — backs desktop/VS Code/LSP)
apps/server/src/runtime-composition.ts 2 (plugin capability bridge + plugin wire)

The plugin-bridge sites matter as much as the RuntimeHost ones: a plugin's bash capability goes through the same sandbox, so missing those would leave plugin-issued writes on the old, narrower root set.

The helper

withAdditionalWritableDirs(sandbox, dirs, cwd?) in core — pure, and deliberately conservative:

  • Never mutates its input.
  • No-op when the sandbox is disabled. It must never enable a sandbox as a side effect of adding a directory, and it must never appear to grant access that isn't actually mediated.
  • Dedupes against existing allowWrite entries, and returns the original object unchanged when nothing new is added.
  • Drops non-absolute entries rather than passing them down. The sandbox profile writers require absolute paths; a bare relative string would be meaningless or, worse, mis-anchored by the time it reached the profile.

/add-dir itself

Validates the path is an existing directory (a file is rejected explicitly), stores it absolute (resolved against cwd — a relative entry would re-anchor to whatever cwd a later session happened to start in), refuses duplicates, and lists the current set when called with no args.

Docs

Only the /add-dir row of BEHAVIOR_PARITY.md changes. #166 rewrote the entire table with June-era content that would have regressed /btw, /voice, /tasks and /background back to unshipped — all four have since landed in #168, #175 and #172. Prettier reflows the table columns on any edit, so git diff -w is the useful view: 2 real lines.

Verification

  • tsc -b --force, pnpm lint (--max-warnings=0), pnpm format:check, pnpm docs:check — clean
  • pnpm test1048 pass, including 9 new helper tests and 6 new /add-dir tests
  • pnpm build — clean

The helper tests pin the properties that make this safe rather than just the happy path: no-mutation, disabled-sandbox no-op, dedupe identity, and relative-entry dropping.

Closes #166.

🤖 Generated with Claude Code

Reworks #166, which was correct in approach but is now incomplete and
carries stale docs.

`permissions.additionalDirectories` has been declared in the settings
schema and read by /permissions for display, but consumed for
enforcement nowhere — /add-dir only printed "recorded ... (effective in
M3)". Users could reasonably believe it did something.

Why the sandbox is the right boundary: the file tools (Read/Write/Edit/
Glob/Grep) already accept any absolute path, so there is no cwd
containment to widen. The only thing that actually restricts writes is
the OS sandbox wrapping Bash. Enforcing /add-dir therefore means adding
those directories to `filesystem.allowWrite`.

The part #166 could not have covered: it predates the app-server, and
wired only CLI REPL + headless. Since #192/#195/#196, desktop, VS Code
and LSP all run through apps/server, so that wiring would have enforced
the setting in the CLI and silently ignored it everywhere else — for a
security-relevant setting, partial enforcement is worse than none,
because it is indistinguishable from full enforcement at the UI. All 7
sandbox assembly sites across 4 files now route through the helper.

- core `withAdditionalWritableDirs(sandbox, dirs, cwd?)`: pure, never
  mutates input, no-op when the sandbox is disabled (never silently
  enables it), dedupes, and drops non-absolute entries rather than
  handing them to profile writers that require absolute paths
- /add-dir validates the path is an existing directory, stores it
  absolute (resolved against cwd), refuses duplicates, and lists the
  current set with no args
- BEHAVIOR_PARITY: only the /add-dir row changes. #166 rewrote the whole
  table with June-era content that would have regressed /btw, /voice,
  /tasks and /background back to unshipped. (Prettier reflows the table
  columns; `git diff -w` shows the 2 real lines.)

tsc -b --force, lint (--max-warnings=0), format:check, docs:check clean;
1048 tests pass (9 new helper + 6 new /add-dir); build clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@oratis
oratis merged commit fb6406f into main Aug 2, 2026
5 checks passed
oratis added a commit that referenced this pull request Aug 2, 2026
…ajors (#215)

Covers the 31-PR Codex alignment stack (#180-#210) plus the dependency
work (#211-#214, #155, #177-#179).

Leads with the security section because that is what this release is:
the central tool gate could be skipped entirely when `mode` was omitted
(#181), and the desktop renderer held provider credentials (#192).

Upgrade notes call out the two things a user could otherwise be
surprised by — the ~6.7 MB → ~115 MB desktop size increase from the Node
sidecar, and that the plugin capability RPC is not an OS boundary.

Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant