Files & Commands

File

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

Usage

Source

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

Source

file.PathMatcher(*, patterns)

Parameter Attributes

patterns: list[str]

file.categorize_files()

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

Usage

Source

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

Source

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

Source

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

Source

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

Source

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: CommandLine
raw: str = ""
cwd: Path | None = None
event: ToolRewriteEvent | None = None
replacements: 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

Source

call(name)
calls()

Every command invocation in the line (nested payloads included), or those named name.

Usage

Source

calls(name=None)
parse()

Parse text into a detached Cmd, or None when it is too deeply nested to parse.

Usage

Source

parse(text)

Call

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

Usage

Source

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: Cmd
occurrence: Occurrence
cwd: 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

A bare substitution word is lifted out of the command’s words entirely, so targets under-counts and sub() cannot re-emit the call faithfully — both refuse accordingly.

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

Source

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 /xtrash /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

Source

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 | None
raw: str
cwd: Path | None

Example

>>> target.is_repo_root or target.contains_repo

Attributes

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

Source

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

Source

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

Source

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

Source

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=val assignments, 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>&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).

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.