---
issue: 44
issue_title: "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)

```text
tool_call event → getPathBearingToolPath → isPathOutsideWorkingDirectory
  → checkPermission("external_directory") → deny/ask/allow
```

### Flow today (bash)

```text
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

```typescript
/**
 * 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:

```typescript
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]+`), so`important_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.
