## Files & Commands


## File


A file path wrapper with glob matching, prefix checks, and test-file detection.


Usage

``` python
File(
    *,
    path,
)
```


Delegates `Path` methods via `__getattr__` so `.suffix`, `.name`, `.parent`, `.exists()` etc. work directly.


#### Parameter Attributes


`path: Path`  


## file.PathMatcher


A reusable set of glob patterns for matching file paths. Supports `in` operator.


Usage

``` python
file.PathMatcher(*, patterns)
```


#### Parameter Attributes


`patterns: list[str]`  


## file.categorize_files()


Split paths into source, test, and skipped buckets for a language.


Usage

``` python
file.categorize_files(paths, *, lang="py")
```


A path that does not match the `lang` globs is skipped; otherwise it is classified as a test file (via `File.is_test`, which treats `conftest.py` and anything under a `tests/` directory as tests) or as source.


#### Parameters


`paths: Iterable[str | Path]`  
File paths to categorize; blank entries are ignored.

`lang: str = ``"py"`  
Language key into `LANG_GLOBS` (defaults to `"py"`); unknown keys fall back to `*.<lang>`.


#### Returns


`list[str]`  
A `(source, test, skipped)` tuple, each a sorted, de-duplicated list of

path strings.


## util.fs.read_json()


Read and parse a JSON file, returning *default* on missing file or parse error.


Usage

``` python
util.fs.read_json(path: Path) -> dict[str, Any] | None
util.fs.read_json(path: Path, default: dict[str, Any]) -> dict[str, Any]
```


## util.fs.resolve_binary()


Resolve an absolute, executable path for *name*, or None.


Usage

``` python
util.fs.resolve_binary(name, *, extra_dirs=())
```


Search order: `$CLAUDE_PLUGIN_ROOT/bin/<name>`, then each *extra_dirs* entry's `<name>`, then `shutil.which(name)`.


## util.fs.binary_supports()


Whether the binary *name* advertises *flag* in its `--help` output.


Usage

``` python
util.fs.binary_supports(name, flag)
```


Resolves *name* through [resolve_binary()](files-commands.md#captain_hook.util.fs.resolve_binary), probes `<path> --help` with a 2s timeout, and tests *flag* for token membership in the combined stdout/stderr -- splitting on runs of non-flag characters, so a bracketed `[--foo]` or a `--foo=VALUE` form still matches. A missing binary, a probe error, or a timeout yields `False`. Memoized for 600s, so a hot path pays the probe at most once per window.


## Cmd


The parsed command line behind `evt.cmd` -- a walk over every command invocation it contains.


Usage

``` python
Cmd(line, raw="", cwd=None, event=None, replacements=dict(), notes=list())
```


`evt.cmd` is always a [Cmd](files-commands.md#captain_hook.Cmd) (an empty line yields zero calls), never None, so a handler never guards before iterating. The walk consumes the parser's payload parts: nested `sh -c`/`eval` payloads and command substitutions surface as calls of their own (three levels deep), and `cd` threads the working directory to the calls after it within the same payload scope. Construct one detached for scanning untrusted payloads: `Cmd(command_line)` over an already-parsed line, or `Cmd.parse(text)` (None when the text is too deeply nested to parse). A detached [Cmd](files-commands.md#captain_hook.Cmd) has no bound event, so [Call.sub()](files-commands.md#captain_hook.Call.sub) raises on it.

`raw` is the true original command text as written: `str(cmd)` returns it, and it is the operand for raw-text matching -- the `~captain_hook.conditions.Command` regex and `ast_grep` search it, so a line that parses to zero commands (a comment, a shebang) still matches by its text. `bool(cmd)` follows `raw`, staying truthy for such a line even when `line` is falsy. [q](files-commands.md#captain_hook.Cmd.q) and `line` expose the parsed structure.


#### Parameter Attributes


`line: CommandLine`  

`raw: str = ``""`  

`cwd: Path | None = None`  

`event: `<a href="events-results.html#captain_hook.ToolRewriteEvent" class="gdls-link gdls-code"><code>ToolRewriteEvent</code></a>` | None = None`  

`replacements: dict[int, str] = dict()`    

`notes: list[str] = list()`    


#### Example

``` python
>>> if (call := evt.cmd.call("rm")) and len(call.targets.expand()) > 10:
...     return call.sub("rm", "trash")
```


#### Attributes

| Name | Description |
|----|----|
| [q](#captain_hook.Cmd.q) | The fluent query over the parsed line -- `CommandLine.q`. |


##### q


The fluent query over the parsed line -- `CommandLine.q`.


`q: CommandLineQuery`


#### Methods

| Name | Description |
|----|----|
| [call()](#captain_hook.Cmd.call) | The first command invocation named [name](files-commands.md#captain_hook.Call.name), or None. |
| [calls()](#captain_hook.Cmd.calls) | Every command invocation in the line (nested payloads included), or those named [name](files-commands.md#captain_hook.Call.name). |
| [parse()](#captain_hook.Cmd.parse) | Parse `text` into a detached [Cmd](files-commands.md#captain_hook.Cmd), or None when it is too deeply nested to parse. |


##### call()


The first command invocation named [name](files-commands.md#captain_hook.Call.name), or None.


Usage

``` python
call(name)
```


##### calls()


Every command invocation in the line (nested payloads included), or those named [name](files-commands.md#captain_hook.Call.name).


Usage

``` python
calls(name=None)
```


##### parse()


Parse `text` into a detached [Cmd](files-commands.md#captain_hook.Cmd), or None when it is too deeply nested to parse.


Usage

``` python
parse(text)
```


## Call


One command invocation reached by walking a command line -- a top-level occurrence, a


Usage

``` python
Call(cmd, occurrence, cwd)
```


nested `sh -c`/`eval` payload, or a command substitution.

[command](files-commands.md#captain_hook.Call.command) is the cc-transcript [Command](files-commands.md#captain_hook.Command) with leading wrappers (`sudo`, `env`, `timeout`, …) stripped arity-aware; [name](files-commands.md#captain_hook.Call.name) is the **dequoted** head word's basename, casefolded (`"rm" -rf /` names the call `rm`), so matching is command-position-only by construction (`echo rm foo` and `git rm` never match `"rm"`) and quoting the executable never evades it.


#### Parameter Attributes


`cmd: `<a href="files-commands.html#captain_hook.Cmd" class="gdls-link gdls-code"><code>Cmd</code></a>  

`occurrence: Occurrence`  

`cwd: Path | None`  


#### Example

``` python
>>> for call in evt.cmd.calls("rm"):
...     if call.targets.expand().exhausted:
...         return evt.block("targets too broad to verify")
...     return call.sub("rm", "trash", args=call.targets) or evt.block("unrecoverable rm")
```


#### Attributes

| Name | Description |
|----|----|
| [args](#captain_hook.Call.args) | The unwrapped command's arguments, with shell quoting removed. |
| [command](#captain_hook.Call.command) | The command with leading wrappers stripped. |
| [flags](#captain_hook.Call.flags) | The option tokens, dequoted, with any registered value-flag's argument (`git -C <dir>`) alongside it. |
| [name](#captain_hook.Call.name) | The unwrapped head word's basename, dequoted and casefolded. |
| [nested](#captain_hook.Call.nested) | Whether this call sits below top level -- a payload or substitution hop away. |
| [redirects](#captain_hook.Call.redirects) | The command's file redirects. |
| [source](#captain_hook.Call.source) | The command as written, wrappers included. |
| [spliceable](#captain_hook.Call.spliceable) | Whether a rewrite can be spliced back -- the source command carries a byte span. |
| [substituted](#captain_hook.Call.substituted) | Whether a command substitution (`$(...)`, backticks) feeds this call's words. |
| [targets](#captain_hook.Call.targets) | The operand tokens as [Target](files-commands.md#captain_hook.Target) objects, value-flag arguments and options removed. |
| [wrappers](#captain_hook.Call.wrappers) | The leading wrapper commands stripped to reach this call, e.g. `("sudo",)`. |


##### args


The unwrapped command's arguments, with shell quoting removed.


`args: tuple[str, …]`


##### command


The command with leading wrappers stripped.


`command: Command`


##### flags


The option tokens, dequoted, with any registered value-flag's argument (`git -C <dir>`) alongside it.


`flags: tuple[str, …]`


##### name


The unwrapped head word's basename, dequoted and casefolded.


`name: str`


##### nested


Whether this call sits below top level -- a payload or substitution hop away.


`nested: bool`


##### redirects


The command's file redirects.


`redirects: tuple[Redirect, …]`


##### source


The command as written, wrappers included.


`source: Command`


##### spliceable


Whether a rewrite can be spliced back -- the source command carries a byte span.


`spliceable: bool`


Substitution payloads, `fish -c` payloads, and joined `eval` payloads carry none. A nested payload with a span splices through its quote layers when the replacement survives them, which [sub()](files-commands.md#captain_hook.Call.sub) checks per rewrite.


##### substituted


Whether a command substitution (`$(...)`, backticks) feeds this call's words.


`substituted: bool`


A bare substitution word is lifted out of the command's words entirely, so [targets](files-commands.md#captain_hook.Call.targets) under-counts and [sub()](files-commands.md#captain_hook.Call.sub) cannot re-emit the call faithfully -- both refuse accordingly.


##### targets


The operand tokens as [Target](files-commands.md#captain_hook.Target) objects, value-flag arguments and options removed.


`targets: `<a href="files-commands.html#captain_hook.Targets" class="gdls-link gdls-code"><code>Targets</code></a>


##### wrappers


The leading wrapper commands stripped to reach this call, e.g. `("sudo",)`.


`wrappers: tuple[str, …]`


Identified by basename against the authoritative `WRAPPER_COMMANDS` table. A wrapper value-flag argument that itself names a wrapper (`sudo -u env rm`) can appear here, but a membership check against a real wrapper head is unaffected.


#### Methods

| Name | Description |
|----|----|
| [sub()](#captain_hook.Call.sub) | Rewrite this call's `old` executable to `new` in place, returning the rewrite result. |


##### sub()


Rewrite this call's `old` executable to `new` in place, returning the rewrite result.


Usage

``` python
sub(old, new, *, args=None, note=None)
```


`old` must equal [name](files-commands.md#captain_hook.Call.name) (else `ValueError` -- a programming error, not a runtime condition). Returns None when the call is not [spliceable](files-commands.md#captain_hook.Call.spliceable), is [substituted](files-commands.md#captain_hook.Call.substituted) (its operands cannot be re-emitted faithfully), or the replacement cannot survive the enclosing quote layers -- so a handler composes the fail-closed fallback in policy code: `call.sub("rm", trash) or evt.block(...)`.

The occurrence's span is replaced with `new` followed by the re-emitted arguments -- each argument's verbatim source spelling (`Word.raw`). [args](files-commands.md#captain_hook.Call.args) defaults to the call's own arguments; pass `args=call.targets` to drop flags and keep only operands, in which case a `-`-leading raw gains a `./` prefix so `new` never misparses it as a flag. Leading wrappers drop (`sudo rm /x` → `trash /x`); redirects outside the span survive.

Subs accumulate on the parent [Cmd](files-commands.md#captain_hook.Cmd): each call returns a fresh rewrite splicing every sub so far, so returning the last applies them all (`rm a && rm b` rewrites both) and returning `evt.block(...)` instead discards them all -- a line never rewrites partially. A [Cmd](files-commands.md#captain_hook.Cmd) that is detached or bound to a non-rewrite event raises `RuntimeError`.


## Target


One operand of a command -- a literal path or a glob -- with resolution and blast-radius predicates.


Usage

``` python
Target(value, raw, cwd)
```


`value` is the operand with shell quoting removed, or None when an unresolved expansion (`$VAR`, `$(...)`) taints the word; such a target is not [verified](files-commands.md#captain_hook.Target.verified) and can never be statically classified -- every predicate returns False and [expand()](files-commands.md#captain_hook.Target.expand) reports itself exhausted, so a guard falls through to its fail-closed branch instead of approving. A glob or tilde operand (`*.py`, `~/x`) keeps its value and stays verified: bash expands it, but to statically known candidates.

[path](files-commands.md#captain_hook.Target.path) resolves the parent directory but keeps the final component **literal**: a symlink is classified as itself, never followed through, which is what a deletion or command target needs (act on the entity named, not what it points at). This is deliberately unlike the [ScratchPath](conditions.md#captain_hook.ScratchPath) condition, which *fully* resolves -- write-approval must see through a symlink to where the bytes actually land. Do not unify the two.

Every predicate returns False when [path](files-commands.md#captain_hook.Target.path) is None (an unverified target, or a relative target with no known cwd).


#### Parameter Attributes


`value: str | None`  

`raw: str`  

`cwd: Path | None`  


#### Example

``` python
>>> target.is_repo_root or target.contains_repo
```


#### Attributes

| Name | Description |
|----|----|
| [contains_repo](#captain_hook.Target.contains_repo) | Whether the target is a directory that contains a git/jj repository. |
| [has_glob](#captain_hook.Target.has_glob) | Whether the target's value contains shell glob metacharacters. |
| [in_repo](#captain_hook.Target.in_repo) | Whether the target lives inside a git/jj repository. |
| [is_fs_root](#captain_hook.Target.is_fs_root) | Whether the target is the filesystem root `/`. |
| [is_home](#captain_hook.Target.is_home) | Whether the target is a home directory (`~` or a top-level `/Users` entry). |
| [is_repo_root](#captain_hook.Target.is_repo_root) | Whether the target is itself a git/jj repository root. |
| [is_scratch](#captain_hook.Target.is_scratch) | Whether the target sits under a temp root or a scratch-named ancestor directory. |
| [path](#captain_hook.Target.path) | The target resolved against `cwd`, parent-resolved with the final component kept literal. |
| [verified](#captain_hook.Target.verified) | Whether the operand is statically known -- False when an expansion taints it. |


##### contains_repo


Whether the target is a directory that contains a git/jj repository.


`contains_repo: bool`


##### has_glob


Whether the target's value contains shell glob metacharacters.


`has_glob: bool`


##### in_repo


Whether the target lives inside a git/jj repository.


`in_repo: bool`


##### is_fs_root


Whether the target is the filesystem root `/`.


`is_fs_root: bool`


##### is_home


Whether the target is a home directory (`~` or a top-level `/Users` entry).


`is_home: bool`


##### is_repo_root


Whether the target is itself a git/jj repository root.


`is_repo_root: bool`


##### is_scratch


Whether the target sits under a temp root or a scratch-named ancestor directory.


`is_scratch: bool`


##### path


The target resolved against `cwd`, parent-resolved with the final component kept literal.


`path: Path | None`


##### verified


Whether the operand is statically known -- False when an expansion taints it.


`verified: bool`


#### Methods

| Name | Description |
|----|----|
| [expand()](#captain_hook.Target.expand) | Expand a glob target to its matches (capped at `limit + 1`); a literal yields itself. |


##### expand()


Expand a glob target to its matches (capped at `limit + 1`); a literal yields itself.


Usage

``` python
expand(*, limit=GLOB_LIMIT)
```


An unverified target expands to nothing, `exhausted` -- it cannot be verified.


## Targets


The operand targets of a command, in order -- iterable, sized, and expandable as a whole.


Usage

``` python
Targets(targets=(), complete=True)
```


`complete` is False when a bare command substitution (`$(...)`, backticks) was lifted out of the command's words, so the collection under-counts the real operands; [verified](files-commands.md#captain_hook.Target.verified) is the one-stop safety check -- every operand present, statically known, and dequoted.


#### Parameter Attributes


`targets: tuple[`<a href="files-commands.html#captain_hook.Target" class="gdls-link gdls-code"><code>Target</code></a>`, …] = ()`    

`complete: bool = ``True`  


#### Attributes

| Name | Description |
|----|----|
| [verified](#captain_hook.Targets.verified) | Whether the operand list is complete and every target is verified. |


##### verified


Whether the operand list is complete and every target is verified.


`verified: bool`


#### Methods

| Name | Description |
|----|----|
| [expand()](#captain_hook.Targets.expand) | Every target's expansion concatenated: literals contribute themselves, globs their matches. |


##### expand()


Every target's expansion concatenated: literals contribute themselves, globs their matches.


Usage

``` python
expand(*, limit=GLOB_LIMIT)
```


Reports itself `exhausted` when any part does or the collection is incomplete.


## Expansion


The paths a target's glob resolves to, plus whether the walk budget was exhausted.


Usage

``` python
Expansion(matches, exhausted)
```


[matches](events-results.md#captain_hook.Turn.matches) is capped at `limit + 1` entries, so `len(expansion) > limit` detects an over-cap glob without materializing every match; `exhausted` is True when a recursive walk blew its 20k-entry budget before completing, or when the targets cannot be statically verified at all -- an unverified target, or an operand list a command substitution lifted a word out of. Either way the expansion is incomplete and a guard must not treat it as safe.


#### Parameter Attributes


`matches: tuple[str, …]`  

`exhausted: bool`  


#### Example

``` python
>>> if len(call.targets.expand()) > 10:
...     return call.sub("rm", "trash")
```


## Command


A single parsed shell command with executable, arguments, env vars, and redirects.


Usage


``` python
Command()
```


Use `Command.parse(raw)` to parse a command string, or access via [CommandLine](files-commands.md#captain_hook.CommandLine).


#### Attributes


`raw: builtins.str`  
The command's source text.

`executable: builtins.str`  
The command name, or "" when nothing parsed.

`args: tuple[str, …]`  
The arguments after the executable.

`env: tuple[tuple[str, str], …]`  
The leading `VAR=val` assignments, as name/value pairs.

`redirects: tuple[`<a href="files-commands.html#captain_hook.Redirect" class="gdls-link gdls-code"><code>Redirect</code></a>`, …]`  
The command's file redirects.

`span: tuple[builtins.int, builtins.int] | None`  
The command's byte span in the line, or None when a redirect absorbed a trailing word; excluded from equality and repr.


#### Attributes

| Name | Description |
|----|----|
| [words](#captain_hook.Command.words) | The structural words: `words[0]` is the executable, `words[1:]` parallel [args](files-commands.md#captain_hook.Call.args). |


##### words


The structural words: `words[0]` is the executable, `words[1:]` parallel [args](files-commands.md#captain_hook.Call.args).


`words: tuple[`<a href="files-commands.html#captain_hook.Word" class="gdls-link gdls-code"><code>Word</code></a>`, …]`


## CommandLine


A full parsed bash command line, potentially containing multiple commands joined by operators.


Usage


``` python
CommandLine()
```


Use `CommandLine.parse(raw)` (or the cached `parse_command_line`) to parse. Access individual commands via `.commands` or the final command via `.primary`.


#### Attributes


`raw: builtins.str`  
The line's source text.

`parts: tuple[tuple[`<a href="files-commands.html#captain_hook.Command" class="gdls-link gdls-code"><code>Command</code></a>`, str | None], …]`  
Each command paired with the operator that follows it (None for the last).

`commands: tuple[`<a href="files-commands.html#captain_hook.Command" class="gdls-link gdls-code"><code>Command</code></a>`, …]`  
The parsed commands, in line order.

`primary: `<a href="files-commands.html#captain_hook.Command" class="gdls-link gdls-code"><code>Command</code></a>` | None`  
The final command, or None when nothing parsed.

`head: `<a href="files-commands.html#captain_hook.Command" class="gdls-link gdls-code"><code>Command</code></a>` | None`  
The first command, or None when nothing parsed.

`prefixes: tuple[str, …]`  
The permission-style prefix of each command, absent prefixes dropped.

`q: CommandLineQuery`  
The predicate helper for this line.

`occurrences: tuple[Occurrence, …]`  
One Occurrence per part, in line order.


#### Methods

| Name | Description |
|----|----|
| [quote()](#captain_hook.CommandLine.quote) | Quote `text` as one POSIX-sh word, with Python `shlex.quote` semantics. |


##### quote()


Quote `text` as one POSIX-sh word, with Python `shlex.quote` semantics.


Usage


``` python
quote(text)
```


## Redirect


A shell redirect parsed from a bash command (e.g. `> file.txt`, `2>&1`).


Usage


``` python
Redirect()
```


#### Attributes


`op: builtins.str`  
The redirect operator (`>`, `>>`, `2>&1` yields `>&`).

`target: builtins.str`  
The redirect target word.

`fd: builtins.int | None`  
The leading file descriptor number, or None when unspecified.


## Word


One structural word of a parsed command ([Command.words](files-commands.md#captain_hook.Command.words)).


Usage


``` python
Word()
```


Attributes: raw: verbatim source text. value: literal with quotes/escapes removed; None when an expansion taints it. span: byte span in the owning `CommandLine.raw`; None without verbatim source. expandable: bash would glob/brace/tilde-expand despite the literal value.
