Files & Commands
File
A file path wrapper with glob matching, prefix checks, and test-file detection.
Usage
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
file.PathMatcher(*, patterns)Parameter Attributes
patterns: list[str]
file.categorize_files()
Split paths into source, test, and skipped buckets for a language.
Usage
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
path strings.(source, test, skipped)tuple, each a sorted, de-duplicated list of
util.fs.read_json()
Read and parse a JSON file, returning default on missing file or parse error.
Usage
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
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
util.fs.binary_supports(name, flag)Resolves name through 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
Cmd(line, raw="", cwd=None, event=None, replacements=dict(), notes=list())evt.cmd is always a 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 has no bound event, so 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 and line expose the parsed structure.
Parameter Attributes
line: CommandLineraw: str = ""cwd: Path | None = Noneevent: ToolRewriteEvent | None = Nonereplacements: dict[int, str] = dict()notes: list[str] = list()
Example
>>> if (call := evt.cmd.call("rm")) and len(call.targets.expand()) > 10:
... return call.sub("rm", "trash")Attributes
| Name | Description |
|---|---|
| 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() | The first command invocation named name, or None. |
| calls() | Every command invocation in the line (nested payloads included), or those named name. |
| parse() |
Parse text into a detached Cmd, or None when it is too deeply nested to parse.
|
call()
The first command invocation named name, or None.
Usage
call(name)calls()
Every command invocation in the line (nested payloads included), or those named name.
Usage
calls(name=None)parse()
Parse text into a detached Cmd, or None when it is too deeply nested to parse.
Usage
parse(text)Call
One command invocation reached by walking a command line — a top-level occurrence, a
Usage
Call(cmd, occurrence, cwd)nested sh -c/eval payload, or a command substitution.
command is the cc-transcript Command with leading wrappers (sudo, env, timeout, …) stripped arity-aware; 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: Cmdoccurrence: Occurrencecwd: Path | None
Example
>>> 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 | The unwrapped command’s arguments, with shell quoting removed. |
| command | The command with leading wrappers stripped. |
| flags |
The option tokens, dequoted, with any registered value-flag’s argument (git -C <dir>) alongside it.
|
| name | The unwrapped head word’s basename, dequoted and casefolded. |
| nested | Whether this call sits below top level — a payload or substitution hop away. |
| redirects | The command’s file redirects. |
| source | The command as written, wrappers included. |
| spliceable | Whether a rewrite can be spliced back — the source command carries a byte span. |
| substituted |
Whether a command substitution ($(...), backticks) feeds this call’s words.
|
| targets | The operand tokens as Target objects, value-flag arguments and options removed. |
| 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() checks per rewrite.
substituted
Whether a command substitution ($(...), backticks) feeds this call’s words.
substituted: bool
targets
The operand tokens as Target objects, value-flag arguments and options removed.
targets: Targets
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() |
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
sub(old, new, *, args=None, note=None)old must equal name (else ValueError — a programming error, not a runtime condition). Returns None when the call is not spliceable, is 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 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: 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 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
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 and can never be statically classified — every predicate returns False and 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 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 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 is None (an unverified target, or a relative target with no known cwd).
Parameter Attributes
value: str | Noneraw: strcwd: Path | None
Example
>>> target.is_repo_root or target.contains_repoAttributes
| Name | Description |
|---|---|
| contains_repo | Whether the target is a directory that contains a git/jj repository. |
| has_glob | Whether the target’s value contains shell glob metacharacters. |
| in_repo | Whether the target lives inside a git/jj repository. |
| is_fs_root |
Whether the target is the filesystem root /.
|
| is_home |
Whether the target is a home directory (~ or a top-level /Users entry).
|
| is_repo_root | Whether the target is itself a git/jj repository root. |
| is_scratch | Whether the target sits under a temp root or a scratch-named ancestor directory. |
| path |
The target resolved against cwd, parent-resolved with the final component kept literal.
|
| 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() |
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
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
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 is the one-stop safety check — every operand present, statically known, and dequoted.
Parameter Attributes
targets: tuple[Target, …] = ()complete: bool = True
Attributes
| Name | Description |
|---|---|
| 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() | Every target’s expansion concatenated: literals contribute themselves, globs their matches. |
expand()
Every target’s expansion concatenated: literals contribute themselves, globs their matches.
Usage
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
Expansion(matches, exhausted)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
>>> 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
Command()Use Command.parse(raw) to parse a command string, or access via 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=valassignments, as name/value pairs. redirects: tuple[Redirect, …]-
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 |
The structural words: words[0] is the executable, words[1:] parallel args.
|
words
The structural words: words[0] is the executable, words[1:] parallel args.
words: tuple[Word, …]
CommandLine
A full parsed bash command line, potentially containing multiple commands joined by operators.
Usage
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[Command, str | None], …]-
Each command paired with the operator that follows it (None for the last).
commands: tuple[Command, …]-
The parsed commands, in line order.
primary: Command | None-
The final command, or None when nothing parsed.
head: Command | 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() |
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
quote(text)Redirect
A shell redirect parsed from a bash command (e.g. > file.txt, 2>&1).
Usage
Redirect()Attributes
op: builtins.str-
The redirect operator (
>,>>,2>&1yields>&). 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).
Usage
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.