loki-mode

loki-seal

Your agent says done. Loki proves it.

loki-seal is a Claude Code Stop hook. When the agent tries to finish, it runs your repo’s real test suite and refuses “done” if tests newly fail or if tests or CI config were deleted, skipped, xfailed or weakened. It runs inside your existing session: no model calls, no global CLI, no dangerous flags, no dependencies beyond node.

Install

In Claude Code: /plugin marketplace add asklokesh/loki-mode, then /plugin install loki-seal@loki-mode. A local checkout also works: /plugin marketplace add <path to a loki-mode checkout>.

The plugin is the enforcing install: its hooks/hooks.json registers SessionStart and Stop. The skill (skills/loki-seal/SKILL.md) is advisory guidance only and registers no hooks, so a skill-only install does not enforce anything.

What it checks

  1. Runner detection: npm test (node –test with default process isolation, launched directly; jest and vitest run without coverage), pytest, go test ./..., cargo test. The suite runs on the current working tree.
  2. A new failing test blocks; so does a suite that ran zero tests. The suite is run once at SessionStart and only failures that are new since then block (baseline-subtract); tests already red are reported, not blamed. If the start run could not happen, every red blocks.
  3. Weakening scan against a snapshot taken at session start (git HEAD if the hook missed SessionStart):
    • removed test file
    • fewer test declarations in a file (removed test functions)
    • fewer assertions in a file
    • new .skip, .only, .todo, xit, xtest, skip: true, @pytest.mark.skip/skipif/xfail, pytest.skip/xfail, @unittest.skip, t.Skip, #[ignore]
    • CI config (GitHub workflows, GitLab, CircleCI, Azure, Jenkinsfile) deleted, test/check lines removed, or softened with continue-on-error: true, || true, if: false
  4. A tree hash over all test files is printed in every receipt.
  5. Delivery contract (bin/contract.js, no model calls, no network). It prefers under-deriving: when in doubt the receipt says “NOT VERIFIED: no contract” and the stop is allowed. The hook reads the first user message from transcript_path. Acceptance items are bullet or checkbox lines and sentences with must, shall, needs to, has to, have to, required or cannot (conversational should, make sure, never and ensure do not count; neither do pasted paths, git status or log lines, or requests that open with a question such as “explain …”; a sentence that opens with I, I’m, I’ve, I’ll, I’d, we, we’re, we’ve, we’ll, let me or let’s, a tool version note such as “Node 20 is required” and a status such as “needs to be done” is ignored as chat, and each such filtered sentence is listed in the contract line as “filtered as chat: N sentence(s)” so a real promise is never dropped silently). Sentences that open with You, Our, My or Please are not filtered: a modal sentence among them is a requirement item. The assertion scan ignores line comments, block comments (including multi-line), Python docstrings and triple-quoted strings, string literals, multi-line template literals and regex literals (a quote or comment marker inside a regex does not open a string or comment), and accepts assert, expect, self.assert*, t.Error/Fatal, testify require./assert., pytest.raises and chai .should. An item needs at least 2 keywords. A local spec file is read only when the request marks it: “spec: docs/x.md”, “per docs/x.md”, “implement docs/x.md” or “according to docs/x.md” (inside the repo, max 5, never URLs); a bare mention such as “fix the typo in README.md” is not a spec. Each item is matched to tests by keyword overlap with test names (at least min(n,2) and ceil(n/2) of its n keywords), and the matched test body must contain an assertion token somewhere in its text (a text scan, not proof the assertion runs; see Known limits). Coverage requires a runner-reported pass: a matched test counts only when its id appears among the tests the runner reported as passed (node:test TAP or spec ok and check-mark lines, pytest PASSED via -rA, go --- PASS:, cargo ... ok, jest/vitest check-mark lines when the runner prints them). A test in a file the runner never ran, or skipped by any mechanism (a skip marker, ctx.skip(), a class-level or module-level pytest or unittest skip, a skip passed through an options variable, describe.skip), is not listed as passed and so never counts, whether the skip is new or already existed; if the runner’s output does not list test names (for example jest without –verbose over several files) the item is NOT VERIFIED rather than assumed covered. Static skip detection (skip, it.skip, describe.skip, xit, test.todo, pytest skip markers, t.Skip, #[ignore]) still drops such tests from matching earlier. An item with no such test, or with ANY matched test failing now (exact test id, regardless of whether it was already red at session start; failing tests never count as covered, so a request item whose tests fail now always blocks), blocks with “NOT VERIFIED” naming the item. A missing or unreadable transcript is NOT VERIFIED with the reason, and the hook still exits cleanly. Contract blocks have their own release counter (LOKI_SEAL_MAX_BLOCKS; a repeated stop when there is no state) and never consume the integrity valve, so a later skipped or deleted test still blocks.

Receipt

loki-seal: PASS
runner: node --test (direct): 12 passed, 0 failed
tests-integrity: intact; baseline: session start
contract: 1 item(s), 1 covered by passing tests
tree: 9f2c41d07ab3e5c8
Verified by Loki https://github.com/asklokesh/loki-mode

Limits

Develop

bash test/run.sh