Skip to main content

ControlLoop CLI

A local-first scanner and deterministic CI gate for what your AI agents can actually do.

An agent's real authority is spread across an agent card, an MCP config, an OpenAPI spec, an IAM policy -- and, for most agents, the framework code itself: a CrewAI crew, a LangGraph graph, an OpenAI Agents SDK handoff chain. No single file tells you that adding one skill just gave it the ability to move money. ControlLoop reads those files, builds one graph of the agent's capabilities, compares it to the state you approved, and fails your build when the agent's authority grows past the ceiling you declared.

It runs entirely on your machine. It never executes your code, never starts your MCP servers, never contacts a declared endpoint, and never sends your source anywhere.

Development preview. init, bom, diff, scan, gate, rules, and example all work and are covered by tests. There is no published release yet — no PyPI package, no Homebrew tap, no container image, no tagged version. Install from source as below. See What is not built yet.


Why use it

  • Catch capability growth in review, not after. A pull request that adds a bulk-refund tool is blocked with the reason, the path, and a retained evidence artifact — before it merges.
  • Declare a ceiling and have it hold. Pattern rules only catch what someone anticipated. A capability ceiling blocks authority you never imagined, through routes no rule matches.
  • Deterministic. Same inputs, same pinned versions, same digest, same verdict. No model decides whether your build passes.
  • Nothing protected leaves. Source, prompts, credentials, environment values, and customer data never appear in output, logs, errors, identity material, or artifacts.
  • Free and local. Core scanning works with the network disabled.

Install and first run

Requires Python 3.12, uv, and Git.

cd cli
uv sync --locked --all-groups
uv run controlloop --version
ControlLoop 0.1.0
Supported OASF versions: none yet
Rule pack: controlloop-community 0.2.0 (10 rules)

Point it at a repository containing an agent:

uv run controlloop scan /path/to/your/agent-repo

Nothing to try it on? The public reference agent is a realistic, deliberately imperfect agent built for exactly this:

git clone https://github.com/danny-tijerina2/controlloop-reference-agent
uv run controlloop scan controlloop-reference-agent/scenarios/order-support-agent

The commands

New here? Run controlloop example — it prints a five-step walkthrough and points at the Example walkthrough guide.

Command Use it to
example Print a guided walkthrough of how ControlLoop works
rules List the rule pack in use and what each rule looks for
init Draft a controlloop.yaml from what's actually in the repo
bom Generate or validate the capability graph (ACBOM)
diff See what changed against a baseline
scan Full report for a human: graph, policy, findings
gate One verdict and one exit code, for CI

scan is what you run at your desk. gate is what you run in CI. Both do the same analysis; gate never prompts, always writes an evidence artifact, and returns a verdict designed to be acted on by a machine.

init

uv run controlloop init [REPOSITORY] [--dry-run]

Discovers candidate agents and writes a reviewable controlloop.yaml. Every field is present even where ControlLoop could not determine a value — those are marked TODO with a comment saying what to supply. It evaluates no policy and enforces nothing.

--dry-run prints the draft instead of writing it. Real output:

# controlloop.yaml
# ControlLoop security manifest -- generated by `controlloop init`.
...
# Manifest schema version 1.1.0.

schema_version: "1.1.0"

# TODO: owner
# ControlLoop cannot determine who is accountable for this
# repository's agents. Replace the placeholder below with an
# identifier for the responsible team or individual (for
# example: "platform-security" or "jane.doe").
owner: "TODO-owner"

trust_boundaries:
  # Discovered candidate agents (from repository discovery):
  #   - invoice-agent  (tools observed: Send an invoice)
  - name: "TODO-trust-boundary-name"

bom

uv run controlloop bom [REPOSITORY] [-o PATH] [--validate] [--format acbom|cyclonedx]

Builds the Agent Capability Bill of Materials — the canonical graph of agents, tools, resources, identities, and permissions, with evidence for every node and edge.

uv run controlloop bom ./agent-repo -o acbom.json
uv run controlloop bom acbom.json --validate
✓ VALID  acbom.json is a valid ACBOM

--validate checks an existing file against this reader's own schema version. Use --format cyclonedx for a CycloneDX projection.

diff

uv run controlloop diff [FIRST] [SECOND] [--baseline REF] [--format text|json]
  • diff <repo> — compare the repository against its resolved baseline (a committed .controlloop/baseline.json, else the merge-base of your default branch).
  • diff <baseline.json> <current.json> — compare two ACBOM files directly.
  • --baseline — an explicit baseline file or git revision.
• BASELINE   committed baseline: .controlloop/baseline.json
• SCOPE      effective authority only -- node and edge changes were not evaluated

• ADDED    capability  clp:agent:sha256:2321fe51…  invokes  clp:tool:sha256:ccc68b72…

1 change: 1 added

scan

uv run controlloop scan [REPOSITORY] [--format text|json|sarif]

Discovery, graph, baseline comparison, and all ten policy rules, rendered as a full report. --format json gives you the complete report; --format sarif gives a SARIF 2.1.0 log for code-scanning tools.

gate

uv run controlloop gate [REPOSITORY] \
  [--evidence-path PATH] [--sarif-path PATH] \
  [--default-branch NAME] [--format text|json|sarif]

The CI form. Returns a four-state verdict (see Reading the output) and always writes an evidence artifact — including on a failing run, which is exactly when you need it.

  • --evidence-path — where the artifact goes (default evidence.json). The parent directory must already exist.
  • --sarif-path — additionally write a SARIF log, carrying the blast-radius statement as runs[0].properties.controlloop.blastRadius.
  • --default-branch — override which branch the merge-base baseline is taken from, for CI checkouts where origin/HEAD is unset.

gate needs real git history for an automatic baseline, so check out with fetch-depth: 0.

Reading the output

Here is a real passing run against the reference agent:

! WORST REACHABLE  financial-action: widgetworks-order-support-agent can invokes
  Issue a refund

✓ PASSED

• material change: not evaluated

! MEDIUM  policy.undeclared-tools: Tool 'Look up an order' is reachable but not
  declared in controlloop.yaml.
    at agent-card.json

Suppressions: none declared

Exit code 0.

1. The blast-radius line comes first. WORST REACHABLE is the single worst thing this agent can do right now — not what changed. It is the line you forward to your manager. Here: this agent can take a financial-action by invoking Issue a refund. If nothing is classified, it reads • no material capability is reachable.

2. The verdict. gate returns one of four:

Verdict Exit Meaning
✓ PASSED 0 Nothing blocking, nothing warned
! PASSED WITH WARNINGS 0 Findings below the blocking threshold
! REVIEW REQUIRED 3 Analysis was incomplete and enforcement is set to fail
✕ DEPLOYMENT BLOCKED 1 A blocking finding

A report can additionally show • NOT EVALUATED, meaning no policy was evaluated at all — which is not the same as evaluating and finding nothing.

Note the glyphs: PASSED WITH WARNINGS uses !, not ✓. Every state is distinguishable by symbol and by word, never by colour alone.

3. Material change — what changed against the baseline, by class. not evaluated means no comparison was possible, which is different from "compared and found nothing" (• no material capability change). ControlLoop never reports an unmade comparison as a clean one.

4. Findings. Each carries a severity glyph and label (✕ HIGH, ! MEDIUM), the rule ID, a plain-language message, and the evidence path. Symbols and words both, never colour alone.

5. Suppressions are always listed, never hidden.

A blocked run

! WORST REACHABLE  destructive-action: widgetworks-order-support-agent can
  invokes Bulk refund orders

✕ DEPLOYMENT BLOCKED

✕ HIGH    policy.destructive-or-financial-action: Agent gained new direct
  invokes access to 'Bulk refund orders', classified as destructive-action and
  financial-action.
    at agent-card.json

Completeness enforcement: warn (default)
Evidence: evidence.json

Exit code 1. Two things to notice:

  • The blast radius got worse — financial-action became destructive-action. The agent's worst case degraded, and that is the first thing the report says.
  • The finding says "gained new". It is not objecting to refunds existing; it is objecting to authority this change introduces against the approved baseline. In --format json that appears as "change_state": "added".

In JSON

{
  "rule_id": "policy.undeclared-tools",
  "severity": "medium",
  "confidence": "high",
  "message": "Tool 'Look up an order' is reachable but not declared in controlloop.yaml.",
  "change_state": "not-compared",
  "remediation": "Declare this tool in controlloop.yaml ... or remove the agent's access to it if it was not intentional.",
  "documentation_url": "https://docs.controlloop.ai/reference/policy-rules#undeclared-tools"
}

Every finding carries its own remediation and a stable documentation_url.

Exit codes

Code Name Meaning
0 PASS No blocking finding
1 POLICY_FAIL A blocking finding
2 INVALID_INPUT Bad manifest, bad path, unreadable ACBOM
3 INCOMPLETE_ANALYSIS Analysis incomplete and enforcement set to fail (REVIEW REQUIRED)
4 ENTITLEMENT Reserved
5 INTERNAL_ERROR A bug — please report it

Telling ControlLoop what your tools do

This is the step people miss. Discovery reads your agent card and your OpenAPI spec, but nothing in a repository states that issuing a refund moves money. Without that, every capability is unclassified and every scan reports no material capability is reachable.

Declare it in controlloop.yaml:

schema_version: "1.1.0"
owner: support-eng

tool_capabilities:
  - tool: Issue a refund        # the DISCOVERED tool name, not its id
    financial_action: true
    read_only: false
    sensitivity: confidential
  - tool: Purge ticket history
    destructive: true

Match the name discovery found, which for an A2A skill is its card name, not its id — Issue a refund, not issue-refund. Run controlloop bom and read the node names if you are unsure; a declaration naming a tool that does not exist is reported, not silently ignored.

Every boolean also accepts "unknown", which the taxonomy ranks worst of all. An unknown risk is never treated as safer than a known one.

Declaring a ceiling

The ceiling is the part that catches what nobody anticipated:

ceilings:
  - agent: widgetworks-order-support-agent
    prohibited_resources:
      - customer-payment-methods
    prohibited_delegation_targets:
      - unapproved-agent

Any reachable capability that breaches this blocks the gate — even with every pattern rule suppressed. A capability-ceiling breach cannot be suppressed.

In CI

- uses: actions/checkout@v7.0.1
  with:
    fetch-depth: 0          # gate needs real history for a baseline
- uses: danny-tijerina2/controlloop@<ref>
  with:
    path: ${{ github.workspace }}

The action runs gate, fails the check on a blocking verdict, uploads SARIF, and writes a check summary. See the GitHub Action reference. A pre-commit hook is also available.

What is not built yet

Stated plainly, so you can plan:

  • No published release. No PyPI package, Homebrew tap, container image, or tagged version. Source install only.
  • No Team tier. Private-repository gates, shared policy bundles, history, attestations, dashboards, and billing do not exist.
  • No runtime protection. ControlLoop analyzes repositories. It does not intercept, sandbox, or kill a running agent.
  • Windows is unsupported.
  • Committed baselines carry no graph, so scan/gate report material change: not evaluated against one. Only the merge-base path computes a material change summary.

Learn more


For maintainers

The lockfile is authoritative. Do not install unpinned project dependencies outside uv.

uv sync --locked --all-groups
uv run ruff format --check .
uv run ruff check .
uv run mypy
uv run pytest
uv build

uv run pytest takes roughly two to three minutes locally, including real PyInstaller builds and a wall-clock performance benchmark that is sensitive to machine load.

Metadata

Release files for controlloop 0.1.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 controlloop 0.1.0
File Size Uploaded Provenance Actions
controlloop-0.1.0.tar.gz 657.8 kB Without provenance Details for controlloop-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for controlloop 0.1.0
File Interpreter ABI Platform Actions
controlloop-0.1.0-py3-none-any.whl Python 3 none any Details for controlloop-0.1.0-py3-none-any.whl

Total release size: 1.0 MB

Release files / controlloop-0.1.0.tar.gz

Download URL controlloop-0.1.0.tar.gz
Size 657.8 kB
Tags Source
SHA-256 checksum
How to use checksums
0921398672fe12b002fdfb7cd12ac4b038e91345dd6a426e92d2fd16acca014d
BLAKE2b-256 checksum
How to use checksums
3fed553e505c2634c2da66319786212a6d78185bba836c8cbffe2d296e6b626b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / controlloop-0.1.0-py3-none-any.whl

Download URL controlloop-0.1.0-py3-none-any.whl
Size 388.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f9440457e90a0235de48d2f1cc3bfc4fd25953197e55472a7c8ce0f6d19c6b68
BLAKE2b-256 checksum
How to use checksums
58067d6ef2f36443c0b636ddedf15699ff23776f4f5baa6ed8a95ff0987d49a6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for controlloop 0.1.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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

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