11 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 44 | Auto-allow /dev/null in external directory checks |
Auto-allow /dev/null in external directory checks
Problem Statement
Agents frequently redirect stderr to /dev/null (e.g., command 2>/dev/null).
The external-directory guard treats /dev/null as a path outside the working directory and prompts for permission.
This is noisy and pointless — /dev/null is universally safe (read returns EOF, write discards data).
The same applies to /dev/stdin, /dev/stdout, and /dev/stderr, which are OS primitives that cannot leak data or modify the filesystem.
Goals
- Add a hardcoded
SAFE_SYSTEM_PATHSset insrc/external-directory.tscontaining/dev/null,/dev/stdin,/dev/stdout,/dev/stderr. - Filter these paths out in
extractExternalPathsFromBashCommandbefore returning. - Filter them out in
isPathOutsideWorkingDirectory(or at its call site ingetPathBearingToolPath/ file-tool check) for the unlikely case a file tool targets/dev/null. - No config changes — this is a universal safety judgment, not policy.
Non-Goals
- Making the allowlist configurable (these are OS primitives).
- Allowing arbitrary paths without prompting.
- Windows
NULdevice — defer until there is a Windows user request.
Background
Permission surface involved
- special / external_directory — the gate this change affects.
Existing modules
| File | Role |
|---|---|
src/external-directory.ts |
isPathOutsideWorkingDirectory, extractExternalPathsFromBashCommand, getPathBearingToolPath, format helpers |
src/index.ts |
Tool-call interceptor; file-tool external-directory gate (~line 828), bash external-directory gate (~line 904) |
Flow today (file tools)
tool_call event → getPathBearingToolPath → isPathOutsideWorkingDirectory
→ checkPermission("external_directory") → deny/ask/allow
Flow today (bash)
tool_call event → extractExternalPathsFromBashCommand
→ if externalPaths.length > 0 → checkPermission("external_directory")
Both flows currently fire on /dev/null because it resolves outside CWD.
Design Overview
SAFE_SYSTEM_PATHS constant
/**
* Paths that are universally safe and should never trigger external-directory checks.
* These are OS device files: read returns EOF or process streams, write discards or goes to process streams.
*/
export const SAFE_SYSTEM_PATHS: ReadonlySet<string> = new Set([
"/dev/null",
"/dev/stdin",
"/dev/stdout",
"/dev/stderr",
]);
isSafeSystemPath helper
A small pure function that checks whether a normalized path is in the safe set:
export function isSafeSystemPath(normalizedPath: string): boolean {
return SAFE_SYSTEM_PATHS.has(normalizedPath);
}
Normalization is already handled by normalizePathForComparison — these paths are absolute and resolve to themselves.
Changes to isPathOutsideWorkingDirectory
After resolving and normalizing the path, check isSafeSystemPath before the CWD comparison.
If the normalized path is a safe system path, return false (not outside working directory).
Changes to extractExternalPathsFromBashCommand
After resolving each candidate token but before adding it to the external paths list, check isSafeSystemPath.
If the candidate's normalized path is a safe system path, skip it.
No changes to src/index.ts
Both the file-tool gate and the bash gate rely on isPathOutsideWorkingDirectory and extractExternalPathsFromBashCommand respectively.
Filtering at the source means no call-site changes are needed.
Merge precedence
No change — no new policy fields.
Module-Level Changes
| File | Change |
|---|---|
src/external-directory.ts |
Add SAFE_SYSTEM_PATHS constant, isSafeSystemPath helper. Update isPathOutsideWorkingDirectory and extractExternalPathsFromBashCommand to skip safe paths. |
tests/external-directory.test.ts |
Add unit tests for isSafeSystemPath, and tests confirming /dev/null et al. are excluded from external-directory checks in both file-tool and bash paths. |
TDD Order
-
Red: Unit tests for
isSafeSystemPath— each safe path returns true, arbitrary paths return false, paths like/dev/null/subdirreturn false. Commit:test: cover isSafeSystemPath for safe system device paths -
Green: Implement
SAFE_SYSTEM_PATHSandisSafeSystemPathinsrc/external-directory.ts. Commit:feat: add SAFE_SYSTEM_PATHS allowlist and isSafeSystemPath helper (#44) -
Red: Tests for
isPathOutsideWorkingDirectoryconfirming/dev/null,/dev/stdin,/dev/stdout,/dev/stderrreturnfalse(not outside CWD) regardless of CWD. Commit:test: isPathOutsideWorkingDirectory skips safe system paths -
Green: Update
isPathOutsideWorkingDirectoryto returnfalsefor safe system paths. Commit:feat: skip safe system paths in isPathOutsideWorkingDirectory (#44) -
Red: Tests for
extractExternalPathsFromBashCommandconfirming commands likecommand 2>/dev/null,cat /dev/stdin, and mixed commands with both/dev/nulland a real external path produce the correct filtered list. Commit:test: extractExternalPathsFromBashCommand filters safe system paths -
Green: Update
extractExternalPathsFromBashCommandto skip safe system paths. Commit:feat: filter safe system paths from bash external path extraction (#44) -
Docs: No config or schema changes needed. Add a brief note in README if the external-directory section mentions the allowlist. Commit:
docs: note safe system path allowlist in external-directory section (#44)
Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Could this silently weaken a permission? | No — the allowlisted paths are OS device files that cannot leak or modify user data. /dev/null discards writes and returns EOF on read. The others map to the process's own stdio streams. |
cat /dev/null > important_file truncates a file inside CWD |
The destructive action is the > redirect to important_file, not the read from /dev/null. extractExternalPathsFromBashCommand splits on > (it is in the metacharacter regex [|;&><\s]+), soimportant_file and /dev/null become separate tokens. important_file is a bare relative name — classifyTokenAsPathCandidate skips it (no leading /,~/, or..). The external-directory gate was never designed to catch in-CWD truncation via bash redirects; that is the bash pattern filter's responsibility. Filtering/dev/null changes nothing about this path. |
cat /dev/null > /etc/passwd truncates an out-of-CWD file |
/dev/null is filtered by the allowlist, but /etc/passwd is a separate token, is an absolute path outside CWD, and still triggers the external-directory check normally. No protection is lost. |
Path traversal via /dev/null/../etc/passwd |
normalizePathForComparison resolves .. before comparison, so this normalizes to /etc/passwd which is not in SAFE_SYSTEM_PATHS. |
Symlink to real file at /dev/null |
On any POSIX system /dev/null is a kernel device node, not a symlink. If an attacker can replace /dev/null they already have root. Out of scope. |
Windows NUL device not covered |
Deferred — no Windows user request yet. The SAFE_SYSTEM_PATHS set can be extended later. |
Open Questions
- Should
/dev/zero,/dev/random,/dev/urandombe included? They are read-only device files but less commonly used by agents. Suggest: defer and add if agents trigger false positives on them.