Skip to main content

kiff-scan

ci claims: machine-verified PyPI Python dependencies: 0 License: MIT

What can your AI agent do without asking?

kiff-scan finds Python functions that are callable by a model and reach an action that deletes data, deploys, moves money, rotates a credential, or runs a shell, with nothing on the path that can refuse.

uvx kiff-scan scan .

No install, no account, no config. Zero dependencies, no network code, and it never executes the code it reads. All three are CI tests, not claims.

Need to show what is governed, what is not, and why? Generate an evidence-backed assessment for an engineering or governance review:

uvx kiff-scan evidence . --format html --output kiff-report.html

The assessment turns the same source evidence into a readiness result, hard blockers, a six-dimension governability scorecard, an action register, and a prioritized remediation plan. Missing evidence is marked not evidenced or not assessable; it is never converted into a pass. See Agent governability assessment.

Output

Run against awslabs/mcp's IAM server at e9f2439:

kiff-scan · what can this agent do without a decision?

  YOUR AGENT'S BLAST RADIUS

  Consequential capabilities: 6
    Decision found on path:   0
    Review required:          6

    Secrets / identity    6   add_user_to_group, attach_user_policy, create_access_key,
                              delete_access_key, delete_user, put_user_policy

  Most exposed: awslabs/iam_mcp_server/server.py:431  delete_user()
    Reachable by:            @tool
    Consequence:             Secrets / identity  (calls delete_access_key())
    Severity:                high
    Match confidence:        call
    Decision on path:        none found on the analysed path
    Model-controlled inputs: user_name, force, confirmed

    State-dependent: an authorization check is necessary but NOT
    sufficient here.

Six tools, six IAM mutations, no decision between the model's argument and the SDK call. Every one is a real @mcp.tool in that repository.

Why an auth check is not enough

Seven of the eight consequence categories are marked state-dependent. A rollback to a broken revision, a failover to a region you already failed over to, a DROP against the live database instead of the drained replica, a refund issued twice before the first settles: each passes every role check you can write, and each is wrong because of when it ran, not who asked.

For those, "add an authorization check" is the wrong fix. The scanner says so instead of implying the check would have been enough.

Accuracy

Ten public repositories, pinned at exact commits, findings labelled by hand:

Files 618
Scored findings 11
Precision 1.00
Recall 0.65
python bench/run.py

Recall is 0.65 because six labelled findings are not reached. All six are the same gap: the destructive call lives in another module. The benchmark includes four repositories where that gap is known to bite, so the number can go down as well as up.

Read the precision honestly. Eleven scored findings is a small set, and four of the ten repositories correctly report nothing, which does not test precision at all. Labels are in bench/expected/, each naming the call that justifies it.

Against the previous release on identical trees:

repo 0.1.1 0.2.0
strands-agents/tools 12 findings, 10 wrong, both dangerous tools missed 4, all correct, both found
pydantic-ai 3, all agent.run() 0
browser-use 2, both select_dropdown 0
agno (human_in_the_loop) 1, ignoring its own confirmation flag 0
awslabs/mcp (IAM) 6, correct 6, correct

What it will not flag

Each has a regression test:

  • agent.run(), chain.run(), client.call(). Execution needs an execution receiver (subprocess, os, asyncio, pexpect, pty) or shell=True.
  • Console().capture(). That is rich's output buffer, not a payment capture.
  • select_dropdown(). Names match on whole tokens, so drop is a token of drop_database and is not a token of dropdown.
  • get_*, list_*, check_* when the finding comes from the name alone. An observed delete_table() in the body still reports.
  • @app.task in a repo that imports no agent framework. That is a Celery job.
  • subprocess.run(["say", text]) reports at low, not high. A fixed program with model-controlled arguments is not a shell.
  • Findings under tests/, examples/, cookbook/, docs/ are listed but not counted. Pass --include-tests to count them.

What it reads from your framework

A tool that declares requires_confirmation=True (agno), needs_approval=True (OpenAI Agents SDK), external_execution=True, or MCP ToolAnnotations(readOnlyHint=True) is treated as governed. Your framework already gates it.

The reverse is a finding of its own:

  ANNOTATION MISMATCH (1)

    warehouse.py:14  inspect_table()
      declares:  readOnlyHint=True
      but:       calls delete_table()

The tool's own metadata is the accuser.

Your code does not leave your machine

Property How it is enforced
No network code anywhere A test rejects any import of socket, ssl, http, urllib, requests, httpx, aiohttp in every module
No telemetry Same gate, plus a check for analytics SDK names
No socket opened during a scan The socket constructor is replaced with one that fails the test, then a real scan runs
Works with networking off A CI job drops all outbound traffic and runs the suite
Never executes your code A test plants a file that would create a sentinel on import; the sentinel must not exist after scanning
Read-only A test intercepts open() and fails on any write during a scan
Zero runtime dependencies A test reads pyproject.toml; CI installs the wheel into a clean venv and fails if anything else appears

These run on every push, every pull request, and daily. Reports contain file paths, line numbers, and parameter names from signatures, never argument values or source text. A test enforces that too.

Detail in docs/THREAT_MODEL.md.

Install

uvx kiff-scan scan .          # no install
uvx kiff-scan evidence .        # management-ready Markdown evidence report
pipx install kiff-scan        # or
pip install kiff-scan

Python 3.10+. In CI:

- uses: kiff/kiff-scan@v0.3.0
  with:
    path: .
    fail-on: high

Limits

  • Python only. Other languages are reported as unsupported, never counted as clean.
  • Calls are followed within a module, up to four hops. A destructive call one import away is missed. This is the largest source of false negatives.
  • Lexical precedence, not control flow. A guard inside if not force: is credited even though a caller can skip it.
  • Guards are recognised by name. An authorize() that always returns True clears a finding.
  • Agent(tools=[fn]) list registration and JSON-schema dispatch tables are not resolved.

A clean scan means no supported path was found. It is not evidence the code is safe. Full list in docs/COVERAGE.md.

Help build the data

The benchmark is a starting point, not a result. Ten repositories and eleven scored findings is enough to catch a regression and not enough to characterise how this behaves across the ecosystem.

Useful contributions, in rough order of value:

  1. A repository where it is wrong. A missed sink, a false positive, a guard it should have honoured, or a clean result on code that is not. Open an issue with the repo, the commit, and the function. Accepted cases become fixtures.
  2. A framework it does not understand. If your tools are registered in a shape that is not in docs/COVERAGE.md, that is a gap worth a fixture even before it is fixed.
  3. A labelled repository for bench/. Add an entry to bench/repos.json with a pinned commit and a bench/expected/<name>.json where every label says which call justifies it. Repositories where the scanner does badly are more useful than ones where it does well.
  4. Numbers from your own codebase. Findings before and after, how many you agreed with, what it missed. Aggregate counts are useful even without the source.

Known misses stay on the list before they are fixed. A challenge list showing only fixed cases is a trophy cabinet.

Docs

  • User guide. Reading a report, explain, CI, config, suppressions, exit codes.
  • Assessment. Readiness rules, evidence states, report formats, and claim boundaries.
  • Coverage. Every registration shape and sink, supported and not.
  • Threat model.

Where this comes from

I build KIFF, a runtime for the decision this scanner asks for. The scanner does not need it, does not talk to it, and works the same without it. Guard detection is vendor-neutral: your own authorize() clears a finding exactly as anything else does.

License

MIT.

Metadata

Release files for kiff-scan 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for kiff-scan 0.3.0
File Size Uploaded Provenance Actions
kiff_scan-0.3.0.tar.gz 98.7 kB With provenance Details for kiff_scan-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for kiff-scan 0.3.0
File Interpreter ABI Platform Actions
kiff_scan-0.3.0-py3-none-any.whl Python 3 none any Details for kiff_scan-0.3.0-py3-none-any.whl

Total release size: 156.6 kB

Release files / kiff_scan-0.3.0.tar.gz

Download URL kiff_scan-0.3.0.tar.gz
Size 98.7 kB
Tags Source
SHA-256 checksum
How to use checksums
631fb12728e1fb1eb0af81ec2b6f92a11e9aaf1574b27ccc83969e8703face08
BLAKE2b-256 checksum
How to use checksums
a84ec8e00679251d89ccc1bd406db17963a0818de27b632a8f370d88c34937fd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.

Transparency log

Release files / kiff_scan-0.3.0-py3-none-any.whl

Download URL kiff_scan-0.3.0-py3-none-any.whl
Size 57.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
07e881c73ff375138eb020dc63642ca4f4963070bf2bdac0ec50fb52c2a18032
BLAKE2b-256 checksum
How to use checksums
b9afb1752d2a6bfc15752352c04704b6c6f0e6156383aae740ccf33a1d036443
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.

Transparency log

Security for kiff-scan 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for kiff_scan-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page