Problem
Four routes confine a caller-supplied filesystem path with isWithinAllowedRoots() and all four reject it with a message that names no path:
server/routes/git.js:34 — path is outside allowed directories (403)
server/routes/commands.js:43 — workspacePath is outside allowed directories (400)
server/routes/scaffold.js:261 — same class of rejection
server/routes/detect.js:70 — valid:false with Path is outside the configured workspace roots (PORTOS_WORKSPACE_ROOTS)
The server log line that reaches the user is therefore just:
❌ Route error [GET /api/git/submodules/status]: path is outside allowed directories
Which path? Which root did it miss? Diagnosing the recent Windows secondary-drive bug took a code read because the log named neither. The same guesswork applies to the other three sites.
Decision (already made — do not re-litigate)
Add one shared formatter to server/lib/workspaceRoots.js:
export function outsideAllowedRootsMessage(realPath, { field = 'path' } = {}) { … }
It renders the rejected realpath'd path and the roots that were checked, e.g.
path is outside allowed directories: <realPath> (allowed: ~, /tmp, /Users, …; on Windows also any non-system drive).
All four call sites use it, so the wording stays identical everywhere and the log names the path.
Chosen over the alternatives:
- Log-only, keep the response terse — rejected: the four sites already differ in status code and wording, and a shared formatter fixes the drift at the same time.
- Per-route ad-hoc strings — rejected: that is what exists today and it drifted.
Privacy note: a realpath'd path embeds the OS home directory, so do not echo it in a response body a user might paste into a public issue — the formatter is for the server-side console line; the HTTP body keeps the existing terse message. (Root CLAUDE.md, "Sensitive Data & Privacy": no home-directory paths in published artifacts.)
Files
server/lib/workspaceRoots.js (new export + README row in server/lib/README.md)
server/routes/git.js, server/routes/commands.js, server/routes/scaffold.js, server/routes/detect.js
- Tests:
server/lib/workspaceRoots.test.js, plus the route suites that assert the current status codes
Acceptance criteria
Context
Deferred from the Windows secondary-drive fix (server/lib/workspaceRoots.js platform-aware roots) to keep that PR to one concern; touching all four routes' messages there would have churned four unrelated test suites.
Problem
Four routes confine a caller-supplied filesystem path with
isWithinAllowedRoots()and all four reject it with a message that names no path:server/routes/git.js:34—path is outside allowed directories(403)server/routes/commands.js:43—workspacePath is outside allowed directories(400)server/routes/scaffold.js:261— same class of rejectionserver/routes/detect.js:70—valid:falsewithPath is outside the configured workspace roots (PORTOS_WORKSPACE_ROOTS)The server log line that reaches the user is therefore just:
Which path? Which root did it miss? Diagnosing the recent Windows secondary-drive bug took a code read because the log named neither. The same guesswork applies to the other three sites.
Decision (already made — do not re-litigate)
Add one shared formatter to
server/lib/workspaceRoots.js:It renders the rejected realpath'd path and the roots that were checked, e.g.
path is outside allowed directories: <realPath> (allowed: ~, /tmp, /Users, …; on Windows also any non-system drive).All four call sites use it, so the wording stays identical everywhere and the log names the path.
Chosen over the alternatives:
Privacy note: a realpath'd path embeds the OS home directory, so do not echo it in a response body a user might paste into a public issue — the formatter is for the server-side
consoleline; the HTTP body keeps the existing terse message. (RootCLAUDE.md, "Sensitive Data & Privacy": no home-directory paths in published artifacts.)Files
server/lib/workspaceRoots.js(new export + README row inserver/lib/README.md)server/routes/git.js,server/routes/commands.js,server/routes/scaffold.js,server/routes/detect.jsserver/lib/workspaceRoots.test.js, plus the route suites that assert the current status codesAcceptance criteria
server/lib/workspaceRoots.js, exported from the barrel and documented inserver/lib/README.mdContext
Deferred from the Windows secondary-drive fix (
server/lib/workspaceRoots.jsplatform-aware roots) to keep that PR to one concern; touching all four routes' messages there would have churned four unrelated test suites.