Skip to content

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)

One rule of a rules file (see the module docs).

name property

name

The rule's id as a catalog name: letters, digits and "_".

triggered

triggered(added)

Added lines matching when (all non-blank added lines without it): where the question is asked.

spec

spec()

The rule as JSON: the checks close over it, so a changed rule changes the catalog's fingerprint.

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).

text

text()

The change as a decider reads it: the file and the added lines.

glob_regex

glob_regex(pattern)

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

load_rules(path)

A rules file (TOML, or JSON by its extension) → [Rule]; RulesError says what is wrong and where.

changes_of

changes_of(payload, root)

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

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

reasons(system, res, change, rules)

The failed checks as (outcome, rule id, line): the rule, the lines, the rule's why; outcome is what the check forces.

words

words(text)

Content words of a text, lower-cased and lightly stemmed (a set).

front_matter

front_matter(text)

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

uninstall(root, agent='claude', dry_run=False)

Remove solvi's hook entries from the project's settings; everything else stays. → the lines it prints.