From 6f77472369b571086a21965592decf38d8e28f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20=C3=81d=C3=A1m?= Date: Thu, 13 Aug 2026 11:44:42 +0200 Subject: [PATCH 1/3] feat: Claude Code Stop-hook integration (check answers with LettuceDetect) --- .../integrations/claude_code/README.md | 75 ++++++ .../integrations/claude_code/__init__.py | 1 + .../integrations/claude_code/check_answer.py | 204 +++++++++++++++++ tests/fixtures/claude_code_transcript.jsonl | 4 + tests/test_claude_code_hook_pytest.py | 216 ++++++++++++++++++ 5 files changed, 500 insertions(+) create mode 100644 lettucedetect/integrations/claude_code/README.md create mode 100644 lettucedetect/integrations/claude_code/__init__.py create mode 100644 lettucedetect/integrations/claude_code/check_answer.py create mode 100644 tests/fixtures/claude_code_transcript.jsonl create mode 100644 tests/test_claude_code_hook_pytest.py diff --git a/lettucedetect/integrations/claude_code/README.md b/lettucedetect/integrations/claude_code/README.md new file mode 100644 index 0000000..5169bab --- /dev/null +++ b/lettucedetect/integrations/claude_code/README.md @@ -0,0 +1,75 @@ +# LettuceDetect + Claude Code + +A [Claude Code hook](https://docs.claude.com/en/docs/claude-code/hooks) that checks +the agent's final answer against grounding context you provide, and feeds flagged +spans back to the agent. With the typed-span models it reports not only unsupported +claims but also `unsupported_addition` — behavior the request never asked for. + +## Installation + +```bash +pip install lettucedetect +``` + +## Try it (5 lines) + +```bash +# 1. Put your grounding passages in context.md in the project root. +# 2. Start the detection server once (keeps the model loaded): +python scripts/start_api.py dev +# 3. Run the hook against a transcript fixture: +echo '{"transcript_path": "tests/fixtures/claude_code_transcript.jsonl"}' | \ + python -m lettucedetect.integrations.claude_code.check_answer --api-url http://127.0.0.1:8000 +``` + +Exit code 0 means the answer is supported; exit code 2 prints a span report on +stderr, which Claude Code feeds back to the agent. + +## Hook configuration + +Paste into your project's `.claude/settings.json`: + +```json +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "python -m lettucedetect.integrations.claude_code.check_answer --api-url http://127.0.0.1:8000 --min-confidence 0.5" + } + ] + } + ] + } +} +``` + +Every time the agent finishes a reply, the hook extracts the final answer from the +session transcript, checks it against `context.md`, and if unsupported spans are +found the agent receives them and revises. + +## Modes + +| Flag | Behavior | +|---|---| +| `--api-url URL` | Uses the running web API (recommended: model stays loaded, one HTTP round trip per check) | +| `--model-path ID` | In-process detection; simplest setup, but loads the model on every invocation | +| `--taxonomy-head ID` | With `--model-path`: types each span (`unsupported_addition`, `contradiction`, ...) | +| `--context-file F` | Grounding passage file, repeatable; default `context.md` | +| `--min-confidence X` | Report only spans at or above this confidence | + +Models: `KRLabsOrg/lettucedect-base-modernbert-en-v1` for prose RAG answers; +`KRLabsOrg/lettucedect-v2-mmbert-base` plus +`KRLabsOrg/lettucedect-v2-taxonomy-head` for code and tool-output answers with +typed spans. + +## Conventions and limits + +- You supply the grounding context (`context.md` or `--context-file`); the hook + does not reconstruct retrieved context from the session. +- If the context file is missing or the transcript has no assistant message, the + hook exits 0 silently. +- When the event carries `stop_hook_active`, the hook exits 0 immediately, so the + agent's revision after a flagged answer is not re-blocked in a loop. diff --git a/lettucedetect/integrations/claude_code/__init__.py b/lettucedetect/integrations/claude_code/__init__.py new file mode 100644 index 0000000..b92dec7 --- /dev/null +++ b/lettucedetect/integrations/claude_code/__init__.py @@ -0,0 +1 @@ +"""Claude Code hook integration: check agent answers with LettuceDetect.""" diff --git a/lettucedetect/integrations/claude_code/check_answer.py b/lettucedetect/integrations/claude_code/check_answer.py new file mode 100644 index 0000000..9780082 --- /dev/null +++ b/lettucedetect/integrations/claude_code/check_answer.py @@ -0,0 +1,204 @@ +"""Claude Code Stop-hook that checks the agent's final answer for hallucinations. + +Reads the hook event JSON from stdin (a ``Stop`` event carries ``transcript_path``), +extracts the last assistant message and the last user message from the transcript, +and checks the answer against user-supplied grounding context. + +Two detector modes: + + --api-url http://127.0.0.1:8000 + Uses the running LettuceDetect web API (``python scripts/start_api.py dev``). + Recommended: the model stays loaded in the server, so the hook costs one + HTTP round trip. + + --model-path KRLabsOrg/lettucedect-base-modernbert-en-v1 + In-process detector. Simplest setup, but the model is loaded on every hook + invocation. Use ``KRLabsOrg/lettucedect-v2-mmbert-base`` (optionally with + ``--taxonomy-head KRLabsOrg/lettucedect-v2-taxonomy-head``) for code and + tool-output answers; the taxonomy head types each span, so additions the + request never asked for are reported as ``unsupported_addition``. + +Exit contract (Claude Code hooks): exit 2 with the report on stderr feeds the +report back to the agent; exit 0 means nothing to report. When the event says +``stop_hook_active`` the hook exits 0 immediately to avoid a feedback loop. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +UNSUPPORTED_ADDITION_NOTE = "the request did not ask for this" + + +def parse_transcript(transcript_path: str | Path) -> tuple[str | None, str | None]: + """Extract (last user message, last assistant message) from a transcript JSONL. + + Transcript lines are JSON objects; conversation entries carry a ``message`` + with ``role`` and ``content`` (a string or a list of content blocks). + Unparseable lines are skipped. + """ + question = None + answer = None + path = Path(transcript_path) + if not path.is_file(): + return None, None + for line in path.read_text(encoding="utf-8").splitlines(): + if not line.strip(): + continue + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + message = entry.get("message") + if not isinstance(message, dict): + continue + text = _message_text(message) + if not text: + continue + if message.get("role") == "user": + question = text + elif message.get("role") == "assistant": + answer = text + return question, answer + + +def _message_text(message: dict) -> str: + content = message.get("content") + if isinstance(content, str): + return content.strip() + if isinstance(content, list): + parts = [ + block.get("text", "") + for block in content + if isinstance(block, dict) and block.get("type") == "text" + ] + return "\n".join(p for p in parts if p).strip() + return "" + + +def load_context(context_files: list[str]) -> list[str]: + """Read grounding passages; missing files are skipped.""" + contexts = [] + for name in context_files: + path = Path(name) + if path.is_file(): + text = path.read_text(encoding="utf-8").strip() + if text: + contexts.append(text) + return contexts + + +def format_report(spans: list[dict]) -> str: + """Human-readable report for flagged spans; handles typed and untyped spans.""" + lines = [f"LettuceDetect flagged {len(spans)} unsupported span(s) in the answer:"] + for span in spans: + confidence = span.get("confidence", span.get("hallucination_score")) + conf = f"confidence {confidence:.2f}" if confidence is not None else "confidence n/a" + category = span.get("category") + if category: + label = category + if span.get("subcategory"): + label += f"/{span['subcategory']}" + if category == "unsupported_addition": + label += f" — {UNSUPPORTED_ADDITION_NOTE}" + lines.append(f'- "{span.get("text", "").strip()}" ({conf}, {label})') + else: + lines.append(f'- "{span.get("text", "").strip()}" ({conf})') + lines.append( + "Revise the flagged parts so every claim is supported by the provided context, " + "or state explicitly that they are not grounded in it." + ) + return "\n".join(lines) + + +def detect_spans_api(api_url: str, contexts: list[str], question: str, answer: str) -> list[dict]: + """Detect spans via a running LettuceDetect web API.""" + from lettucedetect_api.client import LettuceClient + + client = LettuceClient(api_url) + response = client.detect_spans(contexts, question, answer) + return [item.model_dump() for item in response.predictions] + + +def detect_spans_local( + model_path: str, taxonomy_head: str | None, contexts: list[str], question: str, answer: str +) -> list[dict]: + """Detect spans with an in-process detector (loads the model).""" + from lettucedetect.models.inference import HallucinationDetector + + kwargs = {"method": "transformer", "model_path": model_path} + if taxonomy_head: + kwargs["taxonomy_head"] = taxonomy_head + detector = HallucinationDetector(**kwargs) + return detector.predict( + context=contexts, question=question, answer=answer, output_format="spans" + ) + + +def main(argv: list[str] | None = None) -> int: + """Run the hook: parse stdin event, check the answer, report flagged spans.""" + parser = argparse.ArgumentParser( + description="Claude Code Stop-hook: check the final answer with LettuceDetect." + ) + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("--api-url", help="Base URL of a running LettuceDetect web API") + mode.add_argument("--model-path", help="HF model id or local path for in-process detection") + parser.add_argument( + "--taxonomy-head", + help="Optional span-typing head for --model-path (typed spans in the report)", + ) + parser.add_argument( + "--context-file", + action="append", + default=None, + help="Grounding passage file; repeatable (default: context.md in the working directory)", + ) + parser.add_argument( + "--min-confidence", + type=float, + default=0.0, + help="Report only spans at or above this confidence (default 0.0)", + ) + args = parser.parse_args(argv) + + try: + event = json.load(sys.stdin) + except json.JSONDecodeError: + return 0 + if event.get("stop_hook_active"): + return 0 + + transcript_path = event.get("transcript_path") + if not transcript_path: + return 0 + question, answer = parse_transcript(transcript_path) + if not answer: + return 0 + + contexts = load_context(args.context_file or ["context.md"]) + if not contexts: + return 0 + + if args.api_url: + spans = detect_spans_api(args.api_url, contexts, question or "", answer) + else: + spans = detect_spans_local( + args.model_path, args.taxonomy_head, contexts, question or "", answer + ) + + flagged = [ + s + for s in spans + if (s.get("confidence", s.get("hallucination_score")) or 0.0) >= args.min_confidence + ] + if not flagged: + return 0 + print(format_report(flagged), file=sys.stderr) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/fixtures/claude_code_transcript.jsonl b/tests/fixtures/claude_code_transcript.jsonl new file mode 100644 index 0000000..c6e5d7d --- /dev/null +++ b/tests/fixtures/claude_code_transcript.jsonl @@ -0,0 +1,4 @@ +{"type": "user", "message": {"role": "user", "content": [{"type": "text", "text": "What is the population of France?"}]}} +{"type": "assistant", "message": {"role": "assistant", "content": [{"type": "text", "text": "The capital of France is Paris."}, {"type": "text", "text": "The population of France is 69 million."}]}} +{"type": "system", "not_a_message": true} +invalid json line diff --git a/tests/test_claude_code_hook_pytest.py b/tests/test_claude_code_hook_pytest.py new file mode 100644 index 0000000..bd0bbbb --- /dev/null +++ b/tests/test_claude_code_hook_pytest.py @@ -0,0 +1,216 @@ +"""Tests for the Claude Code Stop-hook integration (no model, no network).""" + +import io +import json +from pathlib import Path +from unittest.mock import patch + +import pytest + +from lettucedetect.integrations.claude_code.check_answer import ( + format_report, + load_context, + main, + parse_transcript, +) + +FIXTURE = Path(__file__).parent / "fixtures" / "claude_code_transcript.jsonl" + + +class TestParseTranscript: + """parse_transcript pure-function behavior.""" + + def test_extracts_last_user_and_assistant_messages(self): + """Test extracts last user and assistant messages.""" + question, answer = parse_transcript(FIXTURE) + assert question == "What is the population of France?" + assert answer == ( + "The capital of France is Paris.\nThe population of France is 69 million." + ) + + def test_missing_file_returns_nones(self): + """Test missing file returns nones.""" + question, answer = parse_transcript("does/not/exist.jsonl") + assert question is None and answer is None + + def test_string_content_and_skipped_junk(self, tmp_path): + """Test string content and skipped junk.""" + p = tmp_path / "t.jsonl" + p.write_text( + json.dumps({"message": {"role": "user", "content": "hi"}}) + + "\n" + + "not json\n" + + json.dumps({"message": {"role": "assistant", "content": "hello"}}) + + "\n" + ) + assert parse_transcript(p) == ("hi", "hello") + + +class TestLoadContext: + """load_context file handling.""" + + def test_reads_existing_skips_missing_and_empty(self, tmp_path): + """Test reads existing skips missing and empty.""" + a = tmp_path / "a.md" + a.write_text("passage one") + empty = tmp_path / "empty.md" + empty.write_text(" ") + contexts = load_context([str(a), str(empty), str(tmp_path / "missing.md")]) + assert contexts == ["passage one"] + + +class TestFormatReport: + """format_report output for typed and untyped spans.""" + + def test_untyped_span(self): + """Test untyped span.""" + report = format_report([{"text": " 69 million ", "hallucination_score": 0.93}]) + assert '"69 million" (confidence 0.93)' in report + assert report.startswith("LettuceDetect flagged 1 unsupported span(s)") + + def test_typed_unsupported_addition_gets_note(self): + """Test typed unsupported addition gets note.""" + report = format_report( + [ + { + "text": "added retry logic", + "confidence": 0.88, + "category": "unsupported_addition", + "subcategory": "behavior", + } + ] + ) + assert "unsupported_addition/behavior" in report + assert "the request did not ask for this" in report + + def test_multiple_spans_counted(self): + """Test multiple spans counted.""" + report = format_report([{"text": "a", "confidence": 0.9}, {"text": "b", "confidence": 0.8}]) + assert "2 unsupported span(s)" in report + + +def run_main(argv, event, spans=None, tmp_path=None): + """Run main() with stubbed stdin and a stubbed local detector.""" + + class StubDetector: + """Stub in-process detector.""" + + def __init__(self, **kwargs): + pass + + def predict(self, **kwargs): + return spans or [] + + with ( + patch("sys.stdin", io.StringIO(json.dumps(event))), + patch("lettucedetect.models.inference.HallucinationDetector", StubDetector), + ): + return main(argv) + + +class TestMain: + """End-to-end main() with stubbed detectors.""" + + def make_context(self, tmp_path): + """Make context.""" + ctx = tmp_path / "context.md" + ctx.write_text("France has 67 million inhabitants.") + return str(ctx) + + def test_flagged_answer_exits_2_and_reports(self, tmp_path, capsys): + """Test flagged answer exits 2 and reports.""" + ctx = self.make_context(tmp_path) + code = run_main( + ["--model-path", "stub", "--context-file", ctx], + {"transcript_path": str(FIXTURE)}, + spans=[{"text": "69 million", "confidence": 0.95, "start": 0, "end": 10}], + ) + assert code == 2 + assert "69 million" in capsys.readouterr().err + + def test_clean_answer_exits_0(self, tmp_path, capsys): + """Test clean answer exits 0.""" + ctx = self.make_context(tmp_path) + code = run_main( + ["--model-path", "stub", "--context-file", ctx], + {"transcript_path": str(FIXTURE)}, + spans=[], + ) + assert code == 0 + assert capsys.readouterr().err == "" + + def test_stop_hook_active_short_circuits(self, tmp_path): + """Test stop hook active short circuits.""" + ctx = self.make_context(tmp_path) + code = run_main( + ["--model-path", "stub", "--context-file", ctx], + {"transcript_path": str(FIXTURE), "stop_hook_active": True}, + spans=[{"text": "x", "confidence": 0.99}], + ) + assert code == 0 + + def test_missing_context_exits_0(self, tmp_path): + """Test missing context exits 0.""" + code = run_main( + ["--model-path", "stub", "--context-file", str(tmp_path / "nope.md")], + {"transcript_path": str(FIXTURE)}, + spans=[{"text": "x", "confidence": 0.99}], + ) + assert code == 0 + + def test_min_confidence_filters(self, tmp_path): + """Test min confidence filters.""" + ctx = self.make_context(tmp_path) + code = run_main( + ["--model-path", "stub", "--context-file", ctx, "--min-confidence", "0.9"], + {"transcript_path": str(FIXTURE)}, + spans=[{"text": "x", "confidence": 0.5}], + ) + assert code == 0 + + def test_invalid_stdin_exits_0(self): + """Test invalid stdin exits 0.""" + with patch("sys.stdin", io.StringIO("not json")): + assert main(["--model-path", "stub"]) == 0 + + def test_api_mode_uses_client(self, tmp_path, capsys): + """Test api mode uses client.""" + ctx = self.make_context(tmp_path) + + class StubItem: + """Stub API span item.""" + + def model_dump(self): + return {"text": "69 million", "hallucination_score": 0.97, "start": 0, "end": 10} + + class StubResponse: + """Stub span response.""" + + predictions = (StubItem(),) + + class StubClient: + """Stub HTTP client.""" + + def __init__(self, url): + self.url = url + + def detect_spans(self, contexts, question, answer): + assert contexts and answer + return StubResponse() + + with ( + patch("sys.stdin", io.StringIO(json.dumps({"transcript_path": str(FIXTURE)}))), + patch("lettucedetect_api.client.LettuceClient", StubClient), + ): + code = main(["--api-url", "http://x", "--context-file", ctx]) + assert code == 2 + assert "0.97" in capsys.readouterr().err + + +class TestArgs: + """Argument validation.""" + + def test_mode_required(self): + """Test mode required.""" + with pytest.raises(SystemExit): + main([]) From 295e372220e224f58834b426e26674839b141004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20=C3=81d=C3=A1m?= Date: Thu, 13 Aug 2026 11:53:30 +0200 Subject: [PATCH 2/3] feat: generative detector mode for the Claude Code hook (--llm-model/--llm-base-url) --- .../integrations/claude_code/README.md | 2 + .../integrations/claude_code/check_answer.py | 33 +++++++++++++ tests/test_claude_code_hook_pytest.py | 47 +++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/lettucedetect/integrations/claude_code/README.md b/lettucedetect/integrations/claude_code/README.md index 5169bab..b0c94d8 100644 --- a/lettucedetect/integrations/claude_code/README.md +++ b/lettucedetect/integrations/claude_code/README.md @@ -57,6 +57,8 @@ found the agent receives them and revises. | `--api-url URL` | Uses the running web API (recommended: model stays loaded, one HTTP round trip per check) | | `--model-path ID` | In-process detection; simplest setup, but loads the model on every invocation | | `--taxonomy-head ID` | With `--model-path`: types each span (`unsupported_addition`, `contradiction`, ...) | +| `--llm-model ID` | Generative detector or LLM judge; use `KRLabsOrg/lettucedect-v2-qwen-2b` for typed spans in one pass | +| `--llm-base-url URL` | OpenAI-compatible endpoint for `--llm-model` (e.g. `vllm serve KRLabsOrg/lettucedect-v2-qwen-2b`) | | `--context-file F` | Grounding passage file, repeatable; default `context.md` | | `--min-confidence X` | Report only spans at or above this confidence | diff --git a/lettucedetect/integrations/claude_code/check_answer.py b/lettucedetect/integrations/claude_code/check_answer.py index 9780082..06d412f 100644 --- a/lettucedetect/integrations/claude_code/check_answer.py +++ b/lettucedetect/integrations/claude_code/check_answer.py @@ -18,6 +18,12 @@ tool-output answers; the taxonomy head types each span, so additions the request never asked for are reported as ``unsupported_addition``. + --llm-model KRLabsOrg/lettucedect-v2-qwen-2b --llm-base-url http://localhost:8001/v1 + Generative detector through an OpenAI-compatible endpoint (e.g. vLLM + serving the qwen model). Emits typed spans in one pass. Without + ``--llm-base-url`` the configured provider default is used, so plain LLM + judges (``--llm-model gpt-4.1-mini``) work too. + Exit contract (Claude Code hooks): exit 2 with the report on stderr feeds the report back to the agent; exit 0 means nothing to report. When the event says ``stop_hook_active`` the hook exits 0 immediately to avoid a feedback loop. @@ -138,6 +144,21 @@ def detect_spans_local( ) +def detect_spans_llm( + llm_model: str, base_url: str | None, contexts: list[str], question: str, answer: str +) -> list[dict]: + """Detect spans with the LLM detector (generative lettucedect-v2 models or LLM judges).""" + from lettucedetect.models.inference import HallucinationDetector + + kwargs = {"method": "llm", "model": llm_model} + if base_url: + kwargs["base_url"] = base_url + detector = HallucinationDetector(**kwargs) + return detector.predict( + context=contexts, question=question, answer=answer, output_format="spans" + ) + + def main(argv: list[str] | None = None) -> int: """Run the hook: parse stdin event, check the answer, report flagged spans.""" parser = argparse.ArgumentParser( @@ -146,6 +167,14 @@ def main(argv: list[str] | None = None) -> int: mode = parser.add_mutually_exclusive_group(required=True) mode.add_argument("--api-url", help="Base URL of a running LettuceDetect web API") mode.add_argument("--model-path", help="HF model id or local path for in-process detection") + mode.add_argument( + "--llm-model", + help="Generative detector (e.g. KRLabsOrg/lettucedect-v2-qwen-2b via vLLM) or LLM judge", + ) + parser.add_argument( + "--llm-base-url", + help="OpenAI-compatible endpoint for --llm-model (e.g. a vLLM server)", + ) parser.add_argument( "--taxonomy-head", help="Optional span-typing head for --model-path (typed spans in the report)", @@ -184,6 +213,10 @@ def main(argv: list[str] | None = None) -> int: if args.api_url: spans = detect_spans_api(args.api_url, contexts, question or "", answer) + elif args.llm_model: + spans = detect_spans_llm( + args.llm_model, args.llm_base_url, contexts, question or "", answer + ) else: spans = detect_spans_local( args.model_path, args.taxonomy_head, contexts, question or "", answer diff --git a/tests/test_claude_code_hook_pytest.py b/tests/test_claude_code_hook_pytest.py index bd0bbbb..ac51381 100644 --- a/tests/test_claude_code_hook_pytest.py +++ b/tests/test_claude_code_hook_pytest.py @@ -206,6 +206,48 @@ def detect_spans(self, contexts, question, answer): assert code == 2 assert "0.97" in capsys.readouterr().err + def test_llm_mode_passes_base_url_and_reports_typed_spans(self, tmp_path, capsys): + """Test llm mode passes base url and reports typed spans.""" + ctx = self.make_context(tmp_path) + seen = {} + + class StubDetector: + """Stub LLM detector capturing constructor kwargs.""" + + def __init__(self, **kwargs): + seen.update(kwargs) + + def predict(self, **kwargs): + return [ + { + "text": "added retry logic", + "confidence": 0.9, + "category": "unsupported_addition", + } + ] + + with ( + patch("sys.stdin", io.StringIO(json.dumps({"transcript_path": str(FIXTURE)}))), + patch("lettucedetect.models.inference.HallucinationDetector", StubDetector), + ): + code = main( + [ + "--llm-model", + "KRLabsOrg/lettucedect-v2-qwen-2b", + "--llm-base-url", + "http://localhost:8001/v1", + "--context-file", + ctx, + ] + ) + assert code == 2 + assert seen == { + "method": "llm", + "model": "KRLabsOrg/lettucedect-v2-qwen-2b", + "base_url": "http://localhost:8001/v1", + } + assert "the request did not ask for this" in capsys.readouterr().err + class TestArgs: """Argument validation.""" @@ -214,3 +256,8 @@ def test_mode_required(self): """Test mode required.""" with pytest.raises(SystemExit): main([]) + + def test_modes_mutually_exclusive(self): + """Test modes mutually exclusive.""" + with pytest.raises(SystemExit): + main(["--api-url", "http://x", "--llm-model", "y"]) From 9ff17417b7421abaf4f72b32d9a023a1524832d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20=C3=81d=C3=A1m?= Date: Thu, 13 Aug 2026 14:12:03 +0200 Subject: [PATCH 3/3] fix: suppress model-loading noise in hook stderr --- lettucedetect/integrations/claude_code/check_answer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lettucedetect/integrations/claude_code/check_answer.py b/lettucedetect/integrations/claude_code/check_answer.py index 06d412f..eb6b019 100644 --- a/lettucedetect/integrations/claude_code/check_answer.py +++ b/lettucedetect/integrations/claude_code/check_answer.py @@ -33,9 +33,15 @@ import argparse import json +import os import sys from pathlib import Path +# Keep model-loading noise out of the hook feedback (stderr is fed back to the agent). +os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1") +os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error") +os.environ.setdefault("TQDM_DISABLE", "1") + UNSUPPORTED_ADDITION_NOTE = "the request did not ask for this"