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, andexampleall 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 (defaultevidence.json). The parent directory must already exist.--sarif-path— additionally write a SARIF log, carrying the blast-radius statement asruns[0].properties.controlloop.blastRadius.--default-branch— override which branch the merge-base baseline is taken from, for CI checkouts whereorigin/HEADis 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-actionbecamedestructive-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 jsonthat 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/gatereportmaterial change: not evaluatedagainst one. Only the merge-base path computes a material change summary.
Learn more
- CLI reference — every command and flag
- Terminal report format
- Manifest schema
- Capability ceiling
- Policy rules — all ten
- Material capability taxonomy
- Privacy, security, and limitations
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.