---
issue: 122
issue_title: "piInfrastructureReadPaths doesn't support glob patterns (**), causing confusing fallback to external_directory"
---

# Glob support for `piInfrastructureReadPaths`

## Problem Statement

`piInfrastructureReadPaths` only supports exact directory prefixes via `isPathWithinDirectory()`.
Users who configure glob patterns (e.g. `/opt/homebrew/**/@earendil-works/pi-coding-agent/**`) see silent failures — the path never matches, and reads fall through to the `external_directory` gate.
The docs say the field "supports `~`", but even `~` expansion is missing for plain entries because `path-utils.ts` never calls `expandHomePath()`.

## Goals

- Support `*` and `?` wildcards in `piInfrastructureReadPaths` entries, using the existing `wildcardMatch()` semantics (where `*` matches any characters including `/`).
- Fix `~` expansion for plain (non-glob) directory entries.
- Update schema description, docs, and example config to reflect glob support.
- Fully backward-compatible — plain directory entries keep prefix-match behavior.

## Non-Goals

- Adding true globstar (`**` vs `*` distinction) — the existing wildcard matcher treats them identically, which is fine for path matching.
- Changing the `wildcardMatch()` implementation itself.
- Glob support for the static `piInfrastructureDirs` (computed internally, always absolute paths).

## Background

The infrastructure read bypass is wired as follows:

1. `PermissionSession.getInfrastructureReadPaths()` returns `config.piInfrastructureReadPaths ?? []`.
2. `PermissionGateHandler` merges these with `session.getInfrastructureDirs()` into a single `infraDirs` array.
3. `describeExternalDirectoryGate()` passes `infraDirs` to `isPiInfrastructureRead()`.
4. `isPiInfrastructureRead()` iterates `infrastructureDirs` and calls `isPathWithinDirectory()` — pure prefix matching.

The existing `wildcardMatch()` in `src/wildcard-matcher.ts` already handles `*` (any characters), `?` (one character), and `~`/`$HOME` expansion.
It is used by the rule evaluator for `external_directory` permission patterns, which is why the reporter's workaround with `external_directory` rules worked.

The original plan (archive/0048) explicitly deferred glob support as an open question: "Starting with directory prefixes (simpler).
Globs can be added later if needed."
This issue is that "later."

## Design Overview

The change is contained in `isPiInfrastructureRead()`.
For each entry in `infrastructureDirs`, detect whether it contains glob characters (`*` or `?`).
If it does, use `wildcardMatch(entry, normalizedPath)`.
If it does not, expand `~` via `expandHomePath()` and use the existing `isPathWithinDirectory()` prefix match.

```typescript
import { expandHomePath } from "./expand-home";
import { wildcardMatch } from "./wildcard-matcher";

function containsGlobChars(value: string): boolean {
  return value.includes("*") || value.includes("?");
}

// Inside isPiInfrastructureRead, the loop becomes:
for (const dir of infrastructureDirs) {
  if (containsGlobChars(dir)) {
    if (wildcardMatch(dir, normalizedPath)) return true;
  } else {
    if (isPathWithinDirectory(normalizedPath, expandHomePath(dir))) return true;
  }
}
```

This is backward-compatible: entries without glob characters behave identically (prefix match), with the bonus that `~` now expands correctly.
Entries with glob characters get full wildcard matching.

The `containsGlobChars` helper is a private function — not exported, no new module.

## Module-Level Changes

| File                                   | Change                                                                                                                                                     |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/path-utils.ts`                    | Import `expandHomePath` and `wildcardMatch`. Add private `containsGlobChars()`. Update the loop in `isPiInfrastructureRead()` to branch on glob detection. |
| `schemas/permissions.schema.json`      | Update `piInfrastructureReadPaths` description: remove "no globs" caveat, document `*` and `?` support.                                                    |
| `docs/configuration.md`                | Update the `piInfrastructureReadPaths` row to mention wildcard support and add an example.                                                                 |
| `tests/pi-infrastructure-read.test.ts` | Add tests for glob patterns matching, glob patterns not matching, `~` expansion in plain entries, and mixed glob + plain entries.                          |
| `tests/path-utils.test.ts`             | Add corresponding `isPiInfrastructureRead` glob tests in the existing describe block.                                                                      |

## Test Impact Analysis

1. The new glob branch in `isPiInfrastructureRead()` enables unit tests that were previously impossible: verifying that `*` and `?` patterns in infrastructure dirs match versioned/nested paths.
2. No existing tests become redundant — they all exercise the plain-directory branch which is preserved.
3. All existing `isPiInfrastructureRead` tests must stay as-is; they verify the prefix-match path still works.

## TDD Order

1. **test: glob patterns in `isPiInfrastructureRead`**
   Add failing tests to `tests/pi-infrastructure-read.test.ts`:
   - Glob entry `/opt/homebrew/*/@earendil-works/pi-coding-agent/*` matches a versioned path.
   - Glob entry with `**` behaves the same as `*` (matches across `/`).
   - Glob entry that doesn't match returns false.
   - `?` in a glob entry matches exactly one character.
   - Mixed array of plain dirs and glob patterns — both branches work.
   - Plain entry with `~` prefix now matches (currently broken).
   - Write tool with a glob-matching path is still rejected (read-only guard).

2. **feat: support glob patterns in `piInfrastructureReadPaths`**
   Update `src/path-utils.ts`:
   - Import `expandHomePath` from `./expand-home` and `wildcardMatch` from `./wildcard-matcher`.
   - Add private `containsGlobChars()` helper.
   - Update the `infrastructureDirs` loop in `isPiInfrastructureRead()` to branch on glob detection.
   Commit: `feat: support glob patterns in piInfrastructureReadPaths (#122)`

3. **test: add glob coverage to `path-utils.test.ts`** Add a few representative glob tests to the `isPiInfrastructureRead` describe block in `tests/path-utils.test.ts` to ensure both test files cover the feature.
   Commit: `test: add glob infra-read coverage to path-utils tests`

4. **docs: update schema and docs for glob support**
   - Update `schemas/permissions.schema.json`: change `piInfrastructureReadPaths` description to document `*`, `?`, and `~` support.
   - Update `docs/configuration.md`: revise the `piInfrastructureReadPaths` row and add a glob example.
   Commit: `docs: document piInfrastructureReadPaths glob support (#122)`

## Risks and Mitigations

| Risk                                                                            | Mitigation                                                                                                                                                                                                     |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Overly broad glob pattern (e.g. `/*`) silently auto-allows reads everywhere     | This is a user-configured field with the same trust model as `external_directory: allow` rules. The review log already records every `infrastructure_auto_allowed` bypass, making over-broad patterns visible. |
| `wildcardMatch` treats `*` as `.*` (crosses `/`), so `*` and `**` are identical | This matches the existing wildcard semantics used everywhere else in the permission system. Document it clearly — users don't need to learn a different globbing dialect.                                      |
| Plain entries with `~` were silently broken before this change                  | Adding `expandHomePath()` to the non-glob branch fixes this as a side effect. Backward-compatible because previously `~` entries simply never matched.                                                         |
| `containsGlobChars` false-positive on literal `*` or `?` in a path              | Filesystem paths virtually never contain literal `*` or `?`. On macOS/Linux these characters are legal but extremely rare and strongly discouraged. The risk is negligible.                                    |

## Open Questions

- None — the design reuses existing infrastructure (`wildcardMatch`, `expandHomePath`) with minimal new code.
