---
issue: 39
issue_title: "external_directory check does not cover bash commands referencing paths outside CWD"
---

# Extend `external_directory` gate to bash commands

## Problem Statement

The `external_directory` special permission fires only for path-bearing file tools (`read`, `write`, `edit`, `find`, `grep`, `ls`).
When the agent invokes the `bash` tool with a command that references paths outside CWD (e.g., `cat /etc/hosts`), the check is bypassed entirely.
A user who sets `special.external_directory: "ask"` still has external paths silently accessed through bash.

## Goals

- Bash commands containing tokens that resolve to paths outside CWD trigger the same `external_directory` gate.
- No new config key — reuse `special.external_directory` policy as-is.
- Same precedence as file tools: external directory check fires **before** the bash wildcard pattern check.
- Defense-in-depth heuristic; explicitly not a sandbox.

## Non-Goals

- Full shell parsing (variable expansion, subshells, heredocs, process substitution).
- Catching every possible bypass — this is acknowledged as best-effort tokenization.
- Changing the `bash` permission surface or default policy.
- Modifying the schema or example config (no new fields).

## Background

### Permission surfaces involved

- **special / external_directory** — the gate being extended.
- **bash** — the permission surface that currently handles bash commands independently.

### Existing modules

| File                        | Role                                                                          |
| --------------------------- | ----------------------------------------------------------------------------- |
| `src/external-directory.ts` | `isPathOutsideWorkingDirectory`, `normalizePathForComparison`, format helpers |
| `src/index.ts`              | Tool-call interceptor; currently gates file tools at line ~822                |
| `src/permission-manager.ts` | `checkPermission("external_directory", ...)` resolution                       |
| `src/bash-filter.ts`        | Wildcard pattern matching for bash commands                                   |

### Flow today (file tools)

```text
tool_call event → getPathBearingToolPath → isPathOutsideWorkingDirectory
  → checkPermission("external_directory") → deny/ask/allow
  → (if allowed) normal tool permission check
```

### Flow today (bash)

```text
tool_call event → checkPermission("bash", {command}) → BashFilter.check
  → deny/ask/allow based on wildcard patterns
```

## Design Overview

### New function: `extractExternalPathsFromBashCommand`

Lives in `src/external-directory.ts`.
Accepts `(command: string, cwd: string)` and returns `string[]` of paths that resolve outside CWD.

Tokenization strategy:

1. Split command on shell metacharacters (`|`, `&&`, `||`, `;`, `>`, `<`, whitespace) to isolate tokens.
2. For each token, apply heuristics to decide if it's a path candidate:
   - **Skip** if it starts with `-` (flag).
   - **Skip** if it contains `=` before any `/` (env assignment like `FOO=/bar`).
   - **Skip** if it matches a known non-path pattern (e.g., URL `http://...`, `@scope/package`).
3. Classify path candidates:
   - Absolute: starts with `/`.
   - Home-relative: starts with `~/`.
   - Dot-dot-relative: contains `..` segment.
4. Resolve each candidate via `normalizePathForComparison(token, cwd)`.
5. Test with `isPathOutsideWorkingDirectory`.
6. Return the list of external paths (deduplicated).

### Updated tool-call interceptor in `src/index.ts`

After the existing file-tool external directory block (~line 815) and before the normal `checkPermission` call, add:

```typescript
if (ctx.cwd && toolName === "bash") {
  const command = getNonEmptyString(toRecord(input).command);
  if (command) {
    const externalPaths = extractExternalPathsFromBashCommand(command, ctx.cwd);
    if (externalPaths.length > 0) {
      // Same deny/ask/allow flow as file-tool external_directory
    }
  }
}
```

The deny/ask logic mirrors the existing file-tool block, with adjusted format messages that show the command and extracted paths.

### Format helpers

Add to `src/external-directory.ts`:

- `formatBashExternalDirectoryAskPrompt(command, externalPaths, cwd, agentName?)` — shows the full command, highlighted external paths, and CWD.
- `formatBashExternalDirectoryDenyReason(command, externalPaths, cwd, agentName?)` — same pattern as existing deny reason.

### Merge precedence

No change — `special.external_directory` resolves via the standard global → project → per-agent merge in `PermissionManager`.

## Module-Level Changes

| File                                    | Change                                                                                                                     |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `src/external-directory.ts`             | Add `extractExternalPathsFromBashCommand`, `formatBashExternalDirectoryAskPrompt`, `formatBashExternalDirectoryDenyReason` |
| `src/index.ts`                          | Add bash external-directory gate block before normal `checkPermission`                                                     |
| `tests/external-directory.test.ts`      | Unit tests for `extractExternalPathsFromBashCommand`                                                                       |
| `tests/bash-external-directory.test.ts` | Integration tests for the gate in the tool-call interceptor                                                                |

## TDD Order

1. **Red**: Unit tests for `extractExternalPathsFromBashCommand` — absolute paths, home-relative, dot-dot-relative, within-CWD (no match), flags skipped, env assignments skipped, pipes/semicolons split, URL skipped, `@scope/package` skipped.
   Commit: `test: cover extractExternalPathsFromBashCommand path extraction`

2. **Green**: Implement `extractExternalPathsFromBashCommand` in `src/external-directory.ts`.
   Commit: `feat: extract external paths from bash command tokens (#39)`

3. **Red**: Unit tests for `formatBashExternalDirectoryAskPrompt` and `formatBashExternalDirectoryDenyReason`.
   Commit: `test: cover bash external-directory format helpers`

4. **Green**: Implement format helpers in `src/external-directory.ts`.
   Commit: `feat: add bash external-directory format helpers (#39)`

5. **Red**: Integration tests in `tests/bash-external-directory.test.ts` — bash command with external path triggers deny, triggers ask, passes through on allow, does not fire for in-CWD paths, normal bash pattern still applies after allow.
   Commit: `test: integration tests for bash external_directory gate`

6. **Green**: Wire the gate into `src/index.ts`.
   Commit: `feat: enforce external_directory gate on bash commands (#39)`

7. **Refactor**: Review for any shared logic that can be extracted, ensure review log entries are written for bash external-directory events.
   Commit: `refactor: consolidate bash external-directory review logging (#39)`

## Risks and Mitigations

| Risk                                                                                | Mitigation                                                                                                                                                       |
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| False positives on non-path tokens (regex `/etc/.*`, package `@foo/bar`)            | Skip tokens starting with `@`, skip tokens matching URL patterns, skip tokens without `/` unless they contain `..`                                               |
| Agent bypasses via variable expansion (`$HOME/secret`)                              | Acknowledged as out of scope — defense-in-depth, not sandbox. Document limitation.                                                                               |
| Could this silently weaken a permission?                                            | No — this only *adds* a check. If `external_directory` is `allow`, the new code is a no-op (falls through). Existing bash pattern permissions still apply after. |
| Performance on long commands                                                        | Token extraction is O(n) string splitting; negligible for realistic command lengths.                                                                             |
| Pipe chains with mixed internal/external paths (`ls src/ \| xargs cat /etc/passwd`) | Tokenization catches `/etc/passwd` as external regardless of pipe position.                                                                                      |

## Open Questions

- Should the prompt show all external paths found, or just the first?
  (Suggest: show all, capped at 5, with "and N more" overflow.)
- Should there be a config escape hatch to disable the bash extension of `external_directory` independently?
  (Suggest: defer — if users ask, add `special.bash_external_directory` in a follow-up.)
