Files
my-pi/pi-permission-system/docs/plans/archive/0044-auto-allow-dev-null.md
T

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_PATHS set in src/external-directory.ts containing /dev/null, /dev/stdin, /dev/stdout, /dev/stderr.
  • Filter these paths out in extractExternalPathsFromBashCommand before returning.
  • Filter them out in isPathOutsideWorkingDirectory (or at its call site in getPathBearingToolPath / 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 NUL device — 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

  1. Red: Unit tests for isSafeSystemPath — each safe path returns true, arbitrary paths return false, paths like /dev/null/subdir return false. Commit: test: cover isSafeSystemPath for safe system device paths

  2. Green: Implement SAFE_SYSTEM_PATHS and isSafeSystemPath in src/external-directory.ts. Commit: feat: add SAFE_SYSTEM_PATHS allowlist and isSafeSystemPath helper (#44)

  3. Red: Tests for isPathOutsideWorkingDirectory confirming /dev/null, /dev/stdin, /dev/stdout, /dev/stderr return false (not outside CWD) regardless of CWD. Commit: test: isPathOutsideWorkingDirectory skips safe system paths

  4. Green: Update isPathOutsideWorkingDirectory to return false for safe system paths. Commit: feat: skip safe system paths in isPathOutsideWorkingDirectory (#44)

  5. Red: Tests for extractExternalPathsFromBashCommand confirming commands like command 2>/dev/null, cat /dev/stdin, and mixed commands with both /dev/null and a real external path produce the correct filtered list. Commit: test: extractExternalPathsFromBashCommand filters safe system paths

  6. Green: Update extractExternalPathsFromBashCommand to skip safe system paths. Commit: feat: filter safe system paths from bash external path extraction (#44)

  7. 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/urandom be included? They are read-only device files but less commonly used by agents. Suggest: defer and add if agents trigger false positives on them.