Files
my-pi/pi-permission-system/docs/plans/archive/0123-trailing-wildcard-optionality.md
T

121 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
issue: 123
issue_title: "Support trailing wildcard optionality (`command *` matches bare `command`)"
---
# Trailing wildcard optionality
## Problem Statement
A pattern like `"git *"` only matches commands with arguments (e.g., `"git status"`), not the bare command `"git"`.
Users must write two rules (`"git"` and `"git *"`) to cover both cases.
OpenCode treats the trailing `*` as optional, and users naturally read `"git *"` as "git with anything, including nothing."
## Goals
- Make `compileWildcardPattern` treat a trailing `*` (space + wildcard) as optional, so `"git *"` also matches bare `"git"`.
- Align with OpenCode's behavior for this specific case.
- Update docs to reflect the new behavior.
## Non-Goals
- Supporting the `?` single-character wildcard (separate divergence).
- Changing non-trailing `*` behavior — `"g*t"` still requires content between `g` and `t`.
## Background
The wildcard matcher lives in `src/wildcard-matcher.ts`.
`compileWildcardPattern` splits on `*`, escapes each segment, and joins with `.*`.
The compiled regex is used across all permission surfaces (bash, tools, MCP, skills, special) via `findCompiledWildcardMatch` and `wildcardMatch`.
The change is isolated to one function — `compileWildcardPattern` — because all surfaces use it.
`docs/opencode-compatibility.md` explicitly documents this as a divergence (line 44, line 85105).
`docs/configuration.md` uses `"git *"` in examples that would benefit from the new behavior.
## Design Overview
In `compileWildcardPattern`, after building the escaped regex string, check if it ends with `.*`.
If so, replace the trailing `.*` with `( .*)?` to make the space-and-arguments portion optional.
```typescript
// After joining escaped segments with ".*":
if (escaped.endsWith(" .*")) {
escaped = escaped.slice(0, -3) + "( .*)?";
}
```
This matches OpenCode's implementation exactly.
Edge cases:
- `"git*"` (no space before `*`) — unaffected, still matches `"git"` and `"gitfoo"`.
- `"*"` (lone wildcard) — unaffected, the escaped string is `.*` not `.*`.
- `" *"` (space-only prefix + wildcard) — escaped is `.*`, becomes `( .*)?`, matching empty string and `anything`.
This is an unlikely pattern but harmless.
- `"git status *"` — trailing `.*` becomes optional, so matches both `"git status"` and `"git status --short"`.
Correct.
This broadens existing patterns.
A user who wrote `"rm *": "deny"` would now also block bare `rm`.
This matches user intent and is consistent with the principle that `deny` should err on the side of blocking more.
## Module-Level Changes
### `src/wildcard-matcher.ts`
- In `compileWildcardPattern`: after building the escaped regex string, add the trailing `.*``( .*)?` transformation.
### `tests/wildcard-matcher.test.ts`
- Add tests for trailing wildcard optionality:
1. `"git *"` matches `"git"` (bare command).
2. `"git *"` matches `"git status"` (with arguments — existing behavior preserved).
3. `"git *"` matches `"git status --short"` (multiple arguments).
4. `"git *"` does not match `"npm install"` (different prefix).
5. `"git status *"` matches bare `"git status"`.
6. Non-trailing `*` is unaffected: `"g*t"` does not match `"g"` or `"t"`.
7. `"git*"` (no space) still matches `"git"` — unchanged behavior.
8. `"*"` alone still matches everything — unchanged behavior.
- Update existing test `"glob pattern matches with wildcard"` assertion for `"git *"` against `"git"` (previously `false`, now `true` — or add a new test alongside).
### `docs/opencode-compatibility.md`
- Move "Trailing wildcard optionality" from divergences table to shared concepts.
- Remove the workaround section (lines 85105) or replace it with a note that the behavior now matches.
### `docs/configuration.md`
- Remove the need for duplicate `"git"` + `"git *"` rules in examples where trailing wildcard optionality applies.
## Test Impact Analysis
1. **New tests enabled**: Direct unit tests for the trailing optionality behavior — straightforward additions to the existing `wildcardMatch` and `findCompiledWildcardMatch` describe blocks.
2. **Existing tests that may break**: The test `"glob pattern matches with wildcard"` currently asserts `"git *"` does NOT match bare `"git"` implicitly (the test checks `"git status"` and `"git push origin main"` but not `"git"`).
No existing assertion should break since none test `"git *"` against `"git"`.
However, review all tests to confirm.
3. **Existing tests that stay**: All other wildcard tests (exact match, last-match-wins, regex escaping, home path expansion, multiline) are unaffected.
## TDD Order
1. **Red**: Add tests for trailing wildcard optionality (`"git *"` matches `"git"`, `"git status *"` matches `"git status"`, non-trailing `*` unaffected).
Commit: `test: add trailing wildcard optionality cases (#123)`
2. **Green**: Update `compileWildcardPattern` to apply the `( .*)?` transformation.
Commit: `feat: support trailing wildcard optionality (#123)`
3. **Docs**: Update `docs/opencode-compatibility.md` and `docs/configuration.md` to reflect the new behavior.
Commit: `docs: update wildcard optionality docs (#123)`
## Risks and Mitigations
| Risk | Mitigation |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Broadens existing `deny` patterns (e.g., `"rm *": "deny"` now blocks bare `rm`) | This matches user intent — bare `rm` is also dangerous. Aligns with least-privilege principle. |
| Could silently weaken a permission? | No — the change only broadens what a pattern *matches*, not what decision it produces. A `deny` pattern matching more commands is *more* restrictive, not less. An `allow` pattern matching bare commands is what users expect. |
| Breaks users who rely on `"cmd *"` NOT matching bare `"cmd"` | Unlikely — the current behavior is unintuitive and requires a workaround. The workaround (`"cmd": "allow"` alongside `"cmd *": "allow"`) still works after the change. |
## Open Questions
None — the implementation matches OpenCode's approach exactly and the issue is fully specified.