CLAUDE.md · git:20260512.5c75ebd · 2026-05-12 · sha256 872f4d9e1396512f

CLAUDE.md git:20260512.5c75ebdA

Immutable. This exact content is served forever at /api/v1/blob/872f4d9e1396512f.

# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

Hector is a Rust rewrite of [dynamik-dev/bully](https://github.com/dynamik-dev/bully) — a policy-enforcement pipeline for AI coding agents. Status: **0.1 complete**. All four engines (`script`, `ast`, `semantic`, `session`) are wired. CLI ships `check`, `trust`, `validate`, `init`, `migrate`, `baseline`, `session record`. The Claude Code adapter under `adapters/claude-code/` is shipped (0.1c). Plan 0.2 adds OpenAI + Aider + pre-commit. The authoritative docs are `specs/overview.md` (Hector at 1.0) and `specs/2026-05-11-hector-plan-and-0.1-design.md` (phasing + 0.1 design); the per-phase plans live in `plans/`.

## Rules

- When fixing bugs, start with a failing test. Use the test-writing skill. The failing test will then become the regression coverage.
- Ask for a code review from a separate agent when you complete coding tasks.
- When reviewing code, your code review will be reviewed by our princple engineer, so do good deep work.
- This tool has not shipped yet. We don't need to hedge.
- Rust source files under `crates/*/src/` must meet ≥90% **region** coverage (distinct decision points — branches, short-circuits, match arms — not executed lines). CI enforces this per-file via `scripts/ci-coverage.sh` (cargo-llvm-cov). Adding code without bringing the file up to the gate will break the build.
- Cognitive complexity per function is capped at **15** via clippy (`clippy.toml`, with `#![warn(clippy::cognitive_complexity)]` activated at each crate root). Refactor over annotate; reach for `#[allow(clippy::cognitive_complexity)]` only when the complexity is intrinsic to the function's job and decomposing would scatter the flow, and document why in a comment.
- Mutation testing runs nightly via `.github/workflows/mutants.yml` (cargo-mutants, scoped to `hector-core` at v1). The report uploads as an artifact; surviving mutants don't fail the build yet, but they're the canonical signal for tests that walk the code without verifying behavior — treat survivors in code you touched as a coverage gap, not a curiosity.

## Commands

```bash
cargo build --release                       # produces ./target/release/hector
cargo test                                  # all workspace tests
cargo test -p hector-core                   # core only
cargo test -p hector-cli                    # CLI only
cargo test --test e2e_script_rules          # single integration test file
cargo test <name>                           # filter by test-fn name
cargo clippy --all-targets -- -D warnings   # lint
cargo fmt
bash scripts/ci-coverage.sh                 # per-file ≥90% region-coverage gate (matches CI)
```

Snapshot tests use `insta` — review with `cargo insta review` after intentional verdict-shape changes. CLI tests use `assert_cmd` and shell out to the compiled binary. LLM HTTP paths are exercised with `wiremock` (see `crates/hector-core/tests/anthropic.rs`).

## Architecture

Cargo workspace, two crates:

- **`hector-core`** — library. Modules:
  - `config` — parse v1/v2 YAML, glob scope matching, `extends:` resolution
  - `diff` — unified-diff parser
  - `engine` — `RuleEngine` trait + four impls: `script` (capability-sandboxed exec), `ast` (via `ast-grep-core`), `semantic` (via `LlmClient`), `session` (aggregated cross-edit checks)
  - `llm` — `LlmClient` trait + `AnthropicClient` (blocking `reqwest` with configurable `base_url` for wiremock) + `NoLlm` stub
  - `runner` — orchestrates: load → trust-verify → scope-match → dispatch engine → baseline-filter → telemetry-log
  - `trust` — canonical-YAML sha256 fingerprint
  - `verdict` — Pass/Warn/Block + locked JSON shape
  - `disable` — `hector-disable: <rule-id>` line directives (one rule per directive; the directive ends at whitespace/`*`/`/`)
  - `baseline` — record-and-filter existing violations by `rule_id::file::line` fingerprint
  - `session_state` — `.hector/session.json`, accumulated edits across an agent session
  - `telemetry` — `.hector/log.jsonl`, append-only check log
- **`hector-cli`** — thin binary named `hector`. `cli.rs` defines clap subcommands; `commands/{check,trust,validate,init,migrate,baseline,session}.rs` are one-function adapters that call into core.

Three load-time invariants enforced by `HectorEngine::load` (`crates/hector-core/src/runner.rs`):

1. **Trust gate.** `trust::verify` recomputes the sha256 of the YAML with the `trust:` block stripped and keys sorted; mismatch returns an error (the CLI maps to exit 1). This is the only defense against malicious `script:` rules — capabilities are accident-protection, not adversarial-protection. See `docs/security.md`.
2. **Schema version.** `parser::SUPPORTED_SCHEMAS = [1, 2]`. v1 is legacy bully; `is_legacy()` is the migration hook.
3. **Extends.** `config::extends::resolve` does a cycle-detected DFS; inherited rules fill gaps but **local rules win on collision**, and `trust:` is never inherited.

**Exit-code contract** (`commands/check.rs`):

- `0` — Pass or Warn
- `1` — internal/config error (untrusted, parse failure, missing file)
- `2` — Block (≥1 error-severity violation)

This contract is consumed by CI and editor adapters — do not break it.

**Verdict JSON** (`verdict.rs`) is "locked-but-unstable" at 0.1 and freezes at 0.3. Treat `Verdict`, `Violation`, `Status`, `Severity`, `Engine`, and `SCHEMA_VERSION` as a public stability surface even now — bump `SCHEMA_VERSION` if you must change shape.

**Capability sandbox** (`engine/capability.rs`) is **Linux-strict for network, advisory for writes**. On Linux, `network: false` unshare's the net namespace when privileged (falls back to best-effort with a warning when not). The writes policy is currently a no-op pending CAP_SYS_ADMIN-via-CLONE_NEWUSER work in 0.2. On macOS, all capability constraints are advisory and the command runs unrestricted.

**Scope matching** (`config/scope.rs`) deliberately diverges from raw globset: bare patterns without `/` are also registered as `**/<pattern>` so `*.py` matches at any depth — this mirrors bully's semantics. Don't "fix" it.

**LLM injection.** `Semantic` and `Session` engines need an `LlmClient`. `HectorEngine::load` constructs no LLM (semantic/session rules will then error at evaluation). Tests and library callers inject a fake or real client via `HectorEngine::builder().with_llm(Box::new(...))`.

## Conventions

- New engines plug in via the `EngineKind` enum and a match arm in `runner::check`. All four arms route to real engines today; don't smuggle new logic into an existing arm.
- Test fixtures live in `tests/fixtures/` at the repo root; crate-level tests reference them via relative paths.
- `Cargo.lock` is gitignored (workspace policy in `.gitignore`) — do not commit it.
- The binary name is `hector`, not `hector-cli`.
- `Engine::Trust` in the verdict enum is the catch-all bucket for _internal_ engine errors as well as trust-gate failures (semantic mismatch acknowledged); a rename is on the table before the verdict shape freezes at 0.3.