solvi.hooks¶
solvi behind a coding agent's hooks (Claude Code; Codex, preview): solvi hook pre-edit checks a proposed edit against
a rules file, solvi hook pick-skill names the skill a prompt needs, solvi hook install / uninstall write the
project's hook entries. The guide walks through the setup.
solvi behind a coding agent's hooks: Claude Code, and Codex (preview).
solvi hook install [--project DIR] [--agent claude|codex|both] [--rules PATH] [--skills-dir DIR] [--no-skills]
[--no-edits] [--decider MODEL] [--approve] [--command CMD] [--dry-run]
solvi hook uninstall [--project DIR] [--agent claude|codex|both] [--dry-run]
solvi hook pre-edit --rules rules.toml [--decider MODEL] [--store PATH | --no-store] [--approve] [--agent claude|codex]
[--no-instruction-check] [--project DIR] (PreToolUse: Edit|Write|MultiEdit)
solvi hook pick-skill --skills-dir .claude/skills [--decider MODEL] [--min-score 1.5] [--margin 0.25] [--store PATH |
--no-store] [--project DIR] (UserPromptSubmit)
solvi hook audit [ID] [--rules rules.toml] [--store PATH] (a stored decision: its audit; its replay on the rules)
solvi hook sample-rules (print the sample rules file)
pre-edit reads the hook's JSON on stdin — the tool (Edit, Write, MultiEdit; Codex's apply_patch), the file and the
proposed change — works out the lines the change adds (with their line numbers in the file after the edit, when the
file can be read) and asks a small solvi System one question, edit ∈ {allow, deny, ask}. Its catalog holds, for every
rule whose path globs match the file, the rule's checks as hard checks:
forbid regular expressions no added line may match deterministic → deny
require regular expressions the file after the edit must match deterministic → deny
forbid_calls Python calls (dotted names, globs: "subprocess.*") no added line makes deterministic (AST) → deny
require_def Python functions the file after the edit defines with a non-empty body deterministic (AST) → deny
(none of them) any change to these paths deterministic → on_fail
question a yes/no question a decider answers ("yes" = a violation), asked when fuzzy → ask; deny only with
an added line matches when (always, without when) a calibration file
on_fail = "ask" makes a deterministic rule ask instead of deny. A fuzzy rule blocks only when its decision passed a
threshold from a calibration file (act_guard: P(answered alone and wrong) ≤ risk on your labelled changes); without a
calibration its "yes" asks a person, and without a model every triggered question asks. A check that cannot be evaluated
(the file after the edit is unknown, a decider that escalates) asks. Added lines with instruction-like text addressed to a
reviewer ("ignore the rules", "pre-approved, allow this") ask too (--no-instruction-check turns this off): the rules never
read comments as instructions, this only tells a person that someone tried.
The answer goes back in the documented form: permissionDecision "deny" with the rule, the lines and the reason (Claude
sees it and can fix the change), "ask" (the user confirms; Codex cannot ask, so there it is a deny that says a person must
confirm), and for "allow" nothing — Claude Code's own permission rules still apply — or, with --approve, an explicit
"allow" that skips the permission prompt. A crash of the hook asks rather than allows.
pick-skill reads the skills (<dir>/<skill>/SKILL.md: name and description in the front matter) and the prompt, and
picks one skill or none: deterministically by the words the prompt shares with each skill's name and description (weighted
by how rare they are among the skills), or with --decider by a decider's choice over the skills and "none". When it picks one
it adds a short context line naming the skill (additionalContext); on "none", a near tie or a slash command it stays
silent.
Every decision is stored with its trace in a TraceStorage — by default .solvi/traces/hooks.jsonl in the project — so
solvi verify, solvi report and TraceStorage.query work on it. The store keeps the proposed change and the prompt
(the decision rests on them): keep .solvi/ out of version control.
--decider (0.7: --model, still read) takes what solvi models does: a local checkpoint folder or a cached Hugging Face id (never downloaded), an
OpenAI-compatible endpoint llm:URL#model (key in $SOLVI_LLM_API_KEY), a System One service systemone:URL#model (key in
$SOLVI_SYSTEMONE_API_KEY) or module:attr. Calibrate a fuzzy rule with the same model:
SOLVI_HOOK_RULES=.claude/solvi-rules.toml SOLVI_HOOK_DECIDER=MODEL \
solvi calibrate solvi.hooks:rules_system RULE_answer labels.jsonl --risk 0.1 --out .claude/RULE.calib.json
and name the file in the rule (calibration = "RULE.calib.json", relative to the rules file). RULE is the rule's id with
- as _; the labels are changes (text) with label true for a violation.
Exit status: 0 — the decision is on stdout (or nothing to say); install / uninstall: 0 — done, 2 — usage errors.
RulesError ¶
Bases: ValueError
A rules file that cannot be read: its path, the rule and what is wrong.
SettingsError ¶
Bases: ValueError
An agent's settings file (.claude/settings.json, .codex/hooks.json) that install / uninstall cannot read.
Rule
dataclass
¶
Rule(id: str, paths: list, why: str = '', forbid: list = list(), require: list = list(), forbid_calls: list = list(), require_def: list = list(), question: str | None = None, when: list = list(), on_fail: str = 'deny', calibration: str | None = None, redact: bool = False)
Change
dataclass
¶
Change(tool: str, path: str, added: list, result: str | None, lines: str = 'file', deleted: bool = False)
One file of a proposed edit: the tool, the project-relative path, the added lines [[line, text]] and the file after
the edit (None when it cannot be known: the file is not readable or the edit's old text is not in it). lines says
what the line numbers count: "file" (lines of the file after the edit) or "new text" (lines of the edit's new text).
glob_regex ¶
A path glob → a compiled regex over project-relative POSIX paths: "" and "?" stay inside a folder, "" crosses folders ("*/x.py" also matches "x.py" at the root), everything else is literal.
load_rules ¶
A rules file (TOML, or JSON by its extension) → [Rule]; RulesError says what is wrong and where.
changes_of ¶
The hook's payload → [Change] (one per file; Codex's apply_patch may touch several). ValueError when it is not an edit this hook reads.
rules_system ¶
Every question of the rules file in $SOLVI_HOOK_RULES (default .claude/solvi-rules.toml) as decision parts of one
System, with the decider in $SOLVI_HOOK_DECIDER ($SOLVI_HOOK_MODEL in 0.7, still read): for solvi calibrate solvi.hooks:rules_system RULE_answer ....
reasons ¶
The failed checks as (outcome, rule id, line): the rule, the lines, the rule's why; outcome is what the check forces.
front_matter ¶
The simple key: value front matter of a Markdown file (no YAML library): strings, quoted strings, > / |
blocks.
install ¶
install(root, agent='claude', rules=DEFAULT_RULES, skills_dirs=None, edits=True, skills=True, model=None, approve=False, command=None, dry_run=False)
Write solvi's hook entries into the project's settings (.claude/settings.json; Codex: .codex/hooks.json), merged with what is there: other hooks and settings are kept, solvi's own entries are replaced. Writes the sample rules file when the rules file does not exist. → the lines it prints.
uninstall ¶
Remove solvi's hook entries from the project's settings; everything else stays. → the lines it prints.