{
  "markdown": "# little-canary\n\nPrompt-injection sensing through a powerless sacrificial model.\n\n**Links:** [Website](https://littlecanary.ai) · [Hermes Labs product page](https://hermes-labs.ai/little-canary)\n\nLittle Canary lets untrusted language affect a small model with no application tools or authority, then inspects that model's response for compromise residue before your agent acts. Structural checks catch known input shapes; the distinctive behavioral layer asks what the input *did to the canary*.\n\n```text\nuntrusted text\n    → structural preflight\n    → powerless sacrificial model\n    → response-residue analysis\n    → route: PASS / FLAG / BLOCK, with explicit coverage state\n```\n\nLittle Canary is an inbound risk sensor, not a security guarantee or an agent runtime.\n\n## Technical note\n\n[Behavioral Canarying for Prompt Injection: Powerless Model Probes with Explicit Coverage Semantics](https://hermes-labs.ai/research/behavioral-canarying)\ndocuments Little Canary's pre-execution sensing architecture and the separation\nbetween routing disposition and inspection coverage. It does not claim\nuniversal detection, formal security, or aggregate accuracy for the current\nrelease. Cite the version-independent concept DOI at\n[10.5281/zenodo.21818564](https://doi.org/10.5281/zenodo.21818564):\n\n```bibtex\n@misc{bosch2026behavioralcanarying,\n  author       = {Bosch, Rolando},\n  title        = {Behavioral Canarying for Prompt Injection: Powerless Model\n                  Probes with Explicit Coverage Semantics},\n  year         = {2026},\n  publisher    = {Zenodo},\n  doi          = {10.5281/zenodo.21818564},\n  url          = {https://doi.org/10.5281/zenodo.21818564},\n  note         = {Technical note}\n}\n```\n\nSee [hermes-publications/papers/behavioral-canarying](https://github.com/hermes-labs-ai/hermes-publications/tree/main/papers/behavioral-canarying)\nfor the full evidence boundary.\n\n## Release truth\n\nSource checkouts, GitHub releases, and registry builds are separate evidence\nsurfaces. The version of the source you are reading is recorded in this\nrepository's own metadata (`pyproject.toml` and\n`little_canary/__init__.py`); this README does not assert what any registry\nholds at the moment you read it. For current publication state, consult the\nlive authorities:\n[GitHub Releases](https://github.com/hermes-labs-ai/little-canary/releases)\nand [PyPI](https://pypi.org/project/little-canary/). Historically, GitHub\n`v0.3.1` was source-only and `0.3.2` was intentionally not published or\nreused. The `demo` commands documented below require `0.3.3` or later; verify\nthe installed artifact with `little-canary --version` and confirm it matches\nthe version you intended to install.\n\n## Install\n\nThis source tree supports Python 3.9–3.13. A published artifact's own\npackage metadata is the authority for the Python range that artifact\nadvertises.\n\nFrom the registry (see PyPI for available versions):\n\n```bash\npython -m pip install little-canary\nlittle-canary --version\n```\n\nFor development from a source checkout:\n\n```bash\npython -m pip install .\nlittle-canary --version\n```\n\n## Run the evidence gates without writing Python\n\n### Replay gate: zero egress\n\nThis release, like `0.3.3` before it, deliberately packages no replay\nfixture. The\navailable historical live transcript is incomplete, so turning it into a\nfixture would fabricate missing provenance and response bytes. Therefore this\nexact build reports `REPLAY UNAVAILABLE` and exits `2`:\n\n```bash\nlittle-canary demo --replay\n```\n\nThat is a release hold, not a clean verdict. It makes no model or network call,\ndoes not report risk `0`, and does not silently fall back to live mode.\n\nAfter a complete dedicated live capture is admitted and packaged, the same\ncommand will re-run the shipped analyzer over its versioned clean/attack\nresponse pair. Its first lines will state:\n\n```text\nRUN_KIND   REPLAY\nMODEL_CALL no — recorded output\nCANARY     NOT EXERCISED THIS RUN\nEGRESS     none\n```\n\nSuccess is `REPLAY VERIFIED`: the recorded capture exercised a canary, the current command did not, and the analyzer reproduced the expected contrast. Replay does not prove that a model is installed, reachable, or currently behaves the same way.\n\nA build without an admitted complete capture exits `2` with `REPLAY UNAVAILABLE`; it never invents response bytes or makes a hidden live call.\n\n### Live proof gate: explicit local egress\n\nLive mode requires an endpoint dedicated to this evaluation. A shared or\nunleased runtime is not release evidence; leave the gate unevaluated instead\nof commandeering it.\n\n```bash\nlittle-canary demo --live \\\n  --backend ollama \\\n  --model qwen2.5:1.5b \\\n  --endpoint http://127.0.0.1:11434\n```\n\nLive mode uses a fixed synthetic clean/attack pair and disables the structural filter so the demonstration tests the behavioral mechanism. Before sending either prompt it prints the backend, model, redacted loopback origin, and that raw synthetic input will leave the process. It does not accept arbitrary input and does not fall back to replay.\n\nResults:\n\n- exit `0`: complete clean/non-block plus attack/block contrast;\n- exit `1`: complete calls but `NO CONTRAST` or analyzer expectation mismatch;\n- exit `2`: invalid usage, unavailable model/backend, protocol failure, or otherwise incomplete/degraded run.\n\nAdd `--json` for the agent-readable result. Bare `little-canary demo` exits `2` and requires an explicit `--replay` or `--live` choice.\n\n## Python API\n\n```python\nfrom little_canary import SecurityPipeline\n\npipeline = SecurityPipeline(\n    canary_model=\"qwen2.5:1.5b\",\n    mode=\"full\",\n)\nverdict = pipeline.check(untrusted_text)\n\nif verdict.degraded:\n    # Fail-open routing may still be safe=True, but behavioral coverage failed.\n    quarantine_or_apply_your_availability_policy(untrusted_text)\nelif not verdict.safe:\n    block(untrusted_text, verdict.summary)\nelse:\n    forward_to_agent(verdict.safe_input)\n```\n\nRouting and evidence are separate:\n\n| Field | Meaning |\n|---|---|\n| `safe` | Whether configured routing policy allows forwarding |\n| `degraded` | Whether an enabled required inspection dependency failed |\n| `canary_status` | `exercised`, `failed`, `disabled`, or `skipped_after_block` |\n| `analysis_method` | `regex`, `llm_judge`, or `none` |\n| `analysis_status` | `exercised`, `failed`, or `not_applicable` |\n| `canary_risk_score` | Measured risk, or `None` when no valid measurement exists |\n\nFail-open is availability-first, not a clean verdict. If an enabled canary fails, Little Canary may return `safe=True`, but it also returns `degraded=True`, `canary_status=\"failed\"`, risk `None`, and no PASS label. A failed or skipped layer is never serialized as `passed=true`.\n\nCallbacks follow the same truth boundary: `on_degraded` and `on_unexercised`\nare distinct from `on_pass`. `CanaryGuard` and audit records propagate\ndegraded, `STRUCTURAL_ONLY`, and `UNSCREENED` state.\n\n## Backends and data flow\n\nThe library supports local Ollama and OpenAI-compatible endpoints. The demo intentionally supports loopback Ollama only.\n\n- The canary backend receives the raw input and the known canary system prompt.\n- If an optional LLM judge is configured, it receives the raw input and canary response.\n- A remote endpoint therefore sends data off-machine.\n- AuditLogger omits raw input but stores an unsalted SHA-256 input hash. That supports correlation; it is not anonymity.\n- Runtime inspection found no separate product telemetry path, but provider requests are still egress.\n\nHTTP `200` alone is not successful model coverage. Missing, empty, null, non-string, malformed, timeout, and transport responses are visible protocol failures. Provider bodies, credentials, URL userinfo, and query strings are not included in public errors.\n\n## What “powerless” means\n\nLittle Canary does not give the canary model application tools, credentials, or\noutput execution. The default `SecurityPipeline` strips response bytes and\nsignal-evidence excerpts from its layer snapshot before callbacks or JSON\nserialization; it does not automatically forward canary output to an\nauthoritative agent.\n\nThe low-level `CanaryProbe` and `AnalysisResult` APIs return or retain the\nresponse because analysis requires it. Treat those objects as sensitive: do\nnot execute or forward their contents, and do not attach authority-bearing\ntools to the canary runtime.\n\nThis is a library-level capability boundary, not an operating-system sandbox. If your deployment wraps the model with tools or forwards its output elsewhere, that deployment changes the claim.\n\n## Local HTTP adapter\n\n```bash\nlittle-canary serve \\\n  --port 18421 \\\n  --mode advisory \\\n  --canary-model qwen2.5:1.5b \\\n  --ollama-url http://127.0.0.1:11434\n```\n\nThe server binds to `127.0.0.1`, exposes `GET /health` and `POST /check`, and is unauthenticated. Treat it as a local adapter, not a production gateway.\n\n```bash\ncurl -sS http://127.0.0.1:18421/check \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"text\":\"untrusted text\"}'\n```\n\nEvery accepted non-empty string reaches the pipeline, including one-character input. Malformed, missing, wrong-type, empty, and oversized requests are explicit errors. Text is never silently truncated before inspection. `/health` is liveness-compatible HTTP `200` and includes truthful `ready`, `degraded`, backend, model, and coverage details.\n\nThe loopback server has no authentication, TLS, concurrency hardening, or remote-deployment design in this release.\n\n## Gemini CLI extension\n\nThis repository is also a Gemini CLI extension. It uses Gemini CLI's\n`BeforeAgent` hook to screen the exact current prompt before the agent loop and\ndeny the run when Little Canary returns `safe: false`.\n\nStart the loopback server in blocking mode, then validate and install a source\ncheckout. This integration was verified with Gemini CLI 0.32.1:\n\n```bash\nlittle-canary serve --mode block\ngemini extensions validate .\ngemini extensions install . --consent\n```\n\nThe hook calls only `http://127.0.0.1:18421/check` by default and completes its\nrequest within three seconds. Transport errors, malformed responses, and\nunexercised behavioral coverage are visibly fail-open by default; they are not\nreported as a clean pass. Set `LITTLE_CANARY_FAILURE_MODE=deny` in the Gemini\nprocess environment for fail-closed behavior. `LITTLE_CANARY_ENDPOINT` may\nselect another loopback HTTP `/check` URL, and `LITTLE_CANARY_TIMEOUT_MS` may be\nset from 100 through 5000.\n\nThis extension blocks one Gemini agent run at its pre-agent boundary. It does\nnot establish a general security guarantee or replace least privilege and tool\npolicy.\n\n## Claude Code plugin\n\nThis repository is also a Claude Code marketplace that serves one plugin. The\nplugin uses Claude Code's `UserPromptSubmit` hook to screen the exact submitted\nprompt before the turn starts and block it when Little Canary returns\n`safe: false`.\n\nThe marketplace manifest lives at `.claude-plugin/marketplace.json`, where\nClaude Code discovers it. The plugin itself is the self-contained directory\n`plugins/claude-code/` (plugin manifest, hook registration, and the standalone\nadapter script). Claude Code copies only that directory into its plugin cache,\nso the Gemini CLI extension files at the repository root (`gemini-extension.json`,\n`hooks/hooks.json`) are never installed or loaded by Claude Code.\n\nStart the loopback server in blocking mode, then add this repository as a\nmarketplace and install the plugin. This integration was verified with Claude\nCode 2.1.261:\n\n```bash\nlittle-canary serve --mode block\nclaude plugin marketplace add hermes-labs-ai/little-canary\nclaude plugin install little-canary@hermes-labs\n```\n\nTo validate a source checkout, validate both manifests explicitly. Running\n`claude plugin validate .` from the repository root only validates the\nmarketplace manifest, because the repository root is not itself a plugin:\n\n```bash\nclaude plugin validate .claude-plugin/marketplace.json --strict\nclaude plugin validate plugins/claude-code --strict\n```\n\nTo install from a local checkout instead of GitHub, pass the checkout path to\n`claude plugin marketplace add` and then run the same install command.\n\nThe hook calls only `http://127.0.0.1:18421/check` by default and completes its\nrequest within three seconds. It sends the prompt as the JSON body\n`{\"text\": ...}`. The loopback server rejects request bodies larger than 64 KiB\n(65,536 bytes) with HTTP `413`; that ceiling applies to the encoded JSON body,\nnot to the prompt's character count, so non-ASCII text reaches it sooner. Such\nprompts are not screened at all. Transport errors, HTTP errors including that\n`413`, malformed responses, and unexercised behavioral coverage are visibly\nfail-open by default: the turn continues and Claude Code shows a warning such\nas `Little Canary screening unavailable: HTTPError`, so they are never reported\nas a clean pass. Set `LITTLE_CANARY_FAILURE_MODE=deny` in the Claude Code\nprocess environment to block the turn on every one of those failures instead.\n`LITTLE_CANARY_ENDPOINT` may select another loopback HTTP `/check` URL, and\n`LITTLE_CANARY_TIMEOUT_MS` may be set from 100 through 5000. The hook never\nwrites model context; it emits exactly one JSON object per event.\n\nThis plugin blocks one Claude Code turn at its prompt-submission boundary. It\ndoes not screen tool results, and it does not establish a general security\nguarantee or replace least privilege and tool policy.\n\n## OpenAI Agents SDK input guardrail\n\nThe optional `little_canary.openai_agents` module wraps a `SecurityPipeline`\n(or any object with `check(text) -> PipelineVerdict`) as a native Agents SDK\n`InputGuardrail`. It runs before the agent starts by default.\n\n```bash\npython -m pip install \"little-canary[openai-agents]\"   # Python 3.10+\n```\n\n```python\nfrom agents import Agent\nfrom little_canary import SecurityPipeline\nfrom little_canary.openai_agents import little_canary_input_guardrail\n\npipeline = SecurityPipeline(canary_model=\"qwen2.5:1.5b\", mode=\"block\")\nagent = Agent(name=\"assistant\", input_guardrails=[little_canary_input_guardrail(pipeline)])\n```\n\n`output_info` carries a `coverage` label: `unsafe` trips the SDK tripwire;\n`safe` means behavioral coverage was exercised and clean; `flagged`,\n`degraded`, and `unexercised` are visibly not a PASS. Degraded and unexercised\ncoverage is fail-open by default, matching the pipeline. Pass\n`on_degraded=\"fail_closed\"` to trip the wire unless coverage is exercised\n`safe`. See `examples/openai_agents_example.py`. Offline tests were exercised\nagainst `openai-agents` 0.22.0; importing `little_canary` never requires it.\n\nThe fail-closed option is an explicit caller policy at the SDK boundary. It\ndoes not change `SecurityPipeline` routing or its default fail-open behavior;\nit blocks flagged, degraded, and unexercised outcomes as documented above.\nOnly user-message text is screened. Tool outputs and non-text content are\noutside this adapter's coverage, and SDK input guardrails run only for the\nfirst agent in a chain.\n\n## Evidence labels and limitations\n\nBehavioral evidence is labeled:\n\n- `LIVE`: a model call observed for one exact runtime/model/configuration;\n- `REPLAY`: analyzer behavior over recorded bytes;\n- `MOCK`: controlled protocol or state logic;\n- `STATIC_ONLY`: source/artifact inspection without a model call.\n\nThese labels are not interchangeable. Temperature zero and a seed can improve repeatability but do not guarantee identical model output or classifications across versions, runtimes, or hardware.\n\nThis README makes no aggregate detection, false-positive, latency, or\ntoken-savings claim. Historical benchmark artifacts remain under `benchmarks/`\nwith their limitations and are not a performance certificate for this\nrelease.\n\nLittle Canary should be combined with least privilege, tool policy, data boundaries, monitoring, and output/runtime controls. It does not prove an input harmless, prevent every injection, or replace containment.\n\n## Development\n\n```bash\npytest\nruff check little_canary tests\nmypy little_canary  # diagnostic until the recorded baseline debt is resolved\npython -m build\npython -m twine check dist/*\n```\n\nTests are offline by default and mock network behavior. Live evaluation must use a dedicated endpoint that is not serving another workload.\n\nSee [SECURITY.md](SECURITY.md) for vulnerability reporting and [benchmarks/README.md](benchmarks/README.md) for the current evaluation boundary.\n\n## License\n\nApache-2.0. See [LICENSE](LICENSE).\n",
  "bytes": 16454,
  "sha": "dc852339b99dc55d68573e4d77a896345d9a40937ffafc7bfb54bf58eba8e3c7",
  "repo_slug": "hermes-labs-ai/little-canary",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_hermes_labs_ai_little_canary_1bbcdaee/readme"
}