Trustabl is a static analyzer for agent reliability. It parses an agent-SDK
repository (Claude Agent SDK, OpenAI Agents SDK, Google ADK, MCP, LangChain /
LangGraph, CrewAI, AutoGen / AG2, Pydantic AI, and the Vercel AI SDK), models the
tools, agents, subagents, skills, slash commands, and plugin manifests it
declares, and checks them against a catalog of reliability and safety rules. It reports the weaknesses it finds — each
with an explanation, a suggested fix, and a confidence score — as a
human-readable summary, JSON, or SARIF 2.1.0, plus a per-surface reliability
score and a CI-friendly exit code. It ships as a single Go binary with no
hosted service: it runs as a CLI, or as a local stdio MCP server
(trustabl mcp) that exposes the same scan to MCP clients without opening a
network port.
The rest of this document explains what Trustabl reasons about and how the scan works, then covers building and running it. For the full implementation reference see ARCHITECTURE.md; for the at-a-glance SDK coverage matrix see COVERAGE.md.
What it analyzes — the five-scope model
Trustabl does not treat a repository as one undifferentiated blob. Every rule is classified into exactly one of five scopes, and each scope receives a different typed input:
tool— fires once per tool definition. Input: aToolDef(a@function_tool/@tool/@claude_toolfunction, a Claude TStool(name, description, schema, handler)factory call, aFunctionTool(fn)ADK wrapper, an@server.toolMCP registration, or a bare shell-invoking function) plus its parsed file. Catches a missing docstring, an HTTP call with no timeout, untyped parameters, or an unnormalized path flowing intoopen(). (Hosted tools likeWebSearchTool()are agent-scope edge data, captured asHostedToolDef, notToolDef.)agent— fires once per agent declaration. Input: anAgentDef— a PythonAgent(...)/SandboxAgent(...)/AgentDefinition(...)call, a Claude TS typed-constAgentDefinition, a Claude TS sub-agent inline inoptions.agents, or the Claude TSquery(...)main-thread agent (QueryMainAgent) — with every constructor kwarg captured and its edges to tools, handoffs, and guardrails resolved. Catches an agent with shell tools and noinput_guardrails,tool_use_behavior="stop_on_first_tool"paired with filesystem-touching tools, or a main-thread agent with unrestrictedallowedTools.subagent— fires once per Claude Code subagent markdown declaration. Discovery is hybrid: canonical.claude/agents/*.md(any path depth, monorepo-safe) PLUS a frontmatter-shape fallback over all markdown files (gated onname+tools/model) that catches flat-collection repos which ship subagents undercategories/*.md,plugins/<x>/agents/*.md, or similar layouts. Input: aSubagentDefparsed from frontmatter —name,description,tools[](verbatim) +ToolGrants[](parsed permission grammar),disallowedTools,model,permissionMode(incl.bypassPermissions),mcpServers,skills,isolation,hasHooks. Catches a subagent granted the built-inBashtool despite a read-only description (CSDK-110). Subagent presence alone contributesclaude_agent_sdktoSDKsDetected, so the Claude pack loads and CSDK-110 fires on pure-markdown subagent collections.skill— fires once per Claude Code skill (SKILL.md, any path depth). Input: aSkillDefparsed from frontmatter —name,description,allowed-tools→ToolGrants[],disable-model-invocation— plus body facts (dynamic-context exec commands, external URLs, prompt-injection markers) and a bundled-file inventory. Catches a skill that auto-approves unrestrictedBash(CSKILL-001), runs a dynamic-context command that performs network egress or reads secrets before the model sees it (CSKILL-003), or is model-invocable while granting side-effecting tools (CSKILL-050). Skills are markdown, so skill rules carry nolanguage:; theclaude_skillpack loads whenever aSKILL.mdis present.repo— fires once per scan against the whole inventory. Catches project-wide gaps such as the OpenAI Agents SDK being present with no custom trace processor configured.
The agent is the unit of analysis, not the repo
A repo can declare zero, one, or many agents, across one or more SDKs. Two agents in the same repo can be in completely different security postures — one wired with input/output guardrails, the other not. Agent-scoped findings therefore attribute to a specific agent at its constructor call site; flattening them to a single repo-level verdict would lose that attribution and be wrong. Discovery builds a small per-repo graph (tools, agents, subagents, and the edges between them) so agent-scope and subagent-scope rules can query it.
Rules are scoped to one SDK and one language
A Claude-SDK rule and an OpenAI-Agents-SDK rule that detect the same
conceptual problem (a missing timeout, say) are two separate rules with
SDK-specific explanation and fix text — there is no cross-SDK casting.
When a repo declares agents from multiple SDKs side by side, each agent is
checked only against the rules for the SDK that declared it. The same
holds across languages: a language: python rule will not fire on a
TypeScript agent.
How it reasons — the scanning pipeline
trustabl scans in four steps. Each step's output is the typed input to the next, with no shared state between runs — and the inventory the early steps build is what makes policy selection data-driven rather than statically configured.
The binary ships with no embedded rules. Before the pipeline runs,
Trustabl resolves its detection rules from a separate git repository
(trustabl-rules) —
fetching the latest, caching the clone locally, and falling back to the
cache when the network is unreachable. This decouples rule updates from
binary releases: rules can be added or changed without rebuilding the
scanner. The resolved rules commit is recorded in the result and folded
into the ScanID, so a scan is honest about which rules produced it.
If no rules can be fetched and none are cached, the scan exits 2 and
tells you to run trustabl rules pull — Trustabl never runs rule-less.
flowchart LR
target[("Agent repo<br/>(local path or GitHub URL)")]
recon["Recon<br/>files · SDK deps"]
inv["Inventory<br/>Python + TS AST:<br/>tools · agents ·<br/>subagents · MCP servers"]
pol["Policy selection<br/>load rules per<br/>detected SDK ·<br/>META findings"]
ana["Analysis<br/>tool · agent · subagent ·<br/>repo detectors"]
score["Scoring<br/>per-surface score ·<br/>overall readiness"]
out[("ScanResult<br/>findings · scores<br/>(human / JSON / SARIF)")]
target --> recon --> inv --> pol --> ana --> score --> out
- Recon — walk the repo and answer "what's in here" cheaply, without
parsing any source language: languages present (by extension), SDK
dependencies declared in manifests (
pyproject.toml/requirements.txt/Pipfile/poetry.lock/package.jsonfor theclaude-agent-sdk/@anthropic-ai/claude-agent-sdk/openai-agents/@openai/agents/google-adk/@google/adkneedles), the file inventory, and discovered agent components (MCP configs, hook scripts,CLAUDE.mdandAGENTS.mdguidance docs,.claude/agents/*.mdsubagents at any depth,SKILL.mdskills, slash commands at both.claude/commands/*.mdand<plugin-root>/commands/*.md,.claude-plugin/{plugin,marketplace}.jsonmanifests, sandbox policies). No tree-sitter parses happen here — this step decides whether the expensive AST work is even worth attempting. - Inventory — for each language Recon cleared, do the AST work and
extract a typed inventory:
ToolDefs with their config and body facts,AgentDefs with all kwargs captured,SubagentDefs /SkillDefs /SlashCommandDefs /PluginManifests parsed from markdown and JSON frontmatter,MCPServerDefs, guardrails, sessions, and the resolved edges between agents and the tools/guardrails they reference. Detectors read fields off these structs — they never re-parse raw source. - Policy selection — load only the rule packs for SDKs actually
observed in code. An SDK seen in code with no shipped pack emits a
META-001info finding ("Trustabl does not currently audit this SDK") — silence on an unknown SDK is wrong. A dep declared but never used in code emits a different info finding flagging the drift. - Analysis — run the selected scope-aware detectors against the inventory. Findings carry the scope they fired at and attribute to the right location: tool file/line, agent call site, subagent markdown file, or the manifest.
Three properties fall out of this staging, by design:
- Performance. A repo with no Python skips Python AST work; a repo with only Claude TS code skips Python AST work AND OpenAI policy loading.
- Honest coverage. An "unaudited SDK" info finding is louder than a
zero-findings clean bill of health on an SDK Trustabl doesn't know. A
META-004finding further distinguishes "audited and clean" from "could not audit — discovery extracted nothing a rule targets." - Determinism is a contract. Same inputs → same
ScanID, and the report is byte-stable across runs (findings sorted by(RuleID, FilePath, Line), inventory slices sorted deterministically). CI consumers can diff scans without spurious churn.
See ARCHITECTURE.md § 2 for the full diagram with typed inputs at each step.
What's wired today
Tool/agent AST discovery is wired for:
- Python — Claude Agent SDK (decorators), OpenAI Agents SDK, Google
ADK, LangChain / LangGraph, CrewAI, AutoGen / AG2, and Pydantic AI.
Discovery extracts tool definitions, agent constructors, hosted
tools, MCP servers, guardrails, sessions. The bare
Agent(...)constructor shared by OpenAI / ADK / CrewAI / Pydantic AI is import-gated per SDK so the classes never cross-match, and the shared@tooldecorator is routed to the owning SDK by its import binding. - TypeScript — Claude Agent SDK (the
tool()factory, thequery()main-threadQueryMainAgent, inline-in-query()sub-agents, typed-constAgentDefinitions,createSdkMcpServerand the fouroptions.mcpServersconfig literals), OpenAI Agents SDK (thetool({...})factory,new Agent({...})andAgent.create({...}), 9 hosted-tool factories, MCP server classes across 3 transports plus theMCPServerswrapper, 4defineXguardrail factories, and theMemorySession/OpenAIConversationsSession/OpenAIResponsesCompactionSessionsession classes — gated on imports from@openai/agents,@openai/agents-core, or@openai/agents-openai), and Google ADK (thenew FunctionTool({...})constructor, 5 agent constructors —new LlmAgent({...})/SequentialAgent/ParallelAgent/LoopAgent/RoutedAgent— 13 hosted-tool classes, andsubAgentsedges — gated on imports from@google/adk), LangChain / LangGraph (thetool(fn, {...})factory,DynamicStructuredTool/DynamicTool, andcreateReactAgent/createAgent/new AgentExecutor— gated on the@langchain/*/langchain/langgraphecosystem), and the Vercel AI SDK (thetool({...})/dynamicTool({...})single-object factory, the call-basedgenerateText/streamText/generateObject/streamObjectagents and the classToolLoopAgent/Experimental_Agent, withtoolswalked as an object/record, plus the<provider>.tools.*()hosted tools — gated on the bareaiimport). Handles.ts/.tsx/.mts/.ctsplus JavaScript.js/.jsx/.mjs/.cjswith thetree-sitter-typescriptandtree-sitter-tsxgrammars (JavaScript routes to the tsx grammar — a JS superset — and is audited by the samelanguage: typescriptrule packs). TypeScript rule packs ship for the Claude Agent SDK (CSDK-010/011/012/013/014/016 tool rules; CSDK-120/130/131 agent rules), OpenAI Agents SDK (OAI-016/017/019/022/024 tool rules; OAI-105 agent rule), Google ADK (ADK-013/015/016 tool rules; ADK-109 agent rule), MCP (MCP-011/012/013/014 tool rules), LangChain (LC-010/011/012/013/014 tool rules; LC-111 agent rule), and the Vercel AI SDK (VAI-001..008 tool/agent rules; VAI-012 repo rule). A TS repo for any of these no longer produces a blanketMETA-004; seeCOVERAGE.mdfor the full matrix.
JavaScript (.js / .jsx / .mjs / .cjs) is AST-parsed through the shared
TypeScript-family pipeline: its tools and agents are discovered, tagged
javascript, and audited by the language: typescript rule packs (both ES
import and CommonJS require() bindings are recognized). Go has
tree-sitter-go discovery for MCP tools (mark3labs/mcp-go and the official
modelcontextprotocol/go-sdk), audited by the language: go rules in the MCP
pack. C# has tree-sitter-c-sharp discovery for the official ModelContextProtocol
SDK's [McpServerTool] methods, audited by the language: csharp rules. PHP has
tree-sitter-php discovery for #[McpTool]-attributed methods (official mcp/sdk
and community php-mcp/server), audited by the language: php rules. Rust has
tree-sitter-rust discovery for the official rmcp crate's #[tool]-attributed
methods (descriptions read from the description = "..." arg or the /// doc
comment), audited by the language: rust rules; other Go, .NET, PHP, and Rust
SDKs are recognized as files by Recon but not yet AST-parsed.
The rule schema's language: field gates per-language rule sets.
Scope boundaries
- LLM enrichment is a separate post-scan step (
trustabl enrich). Rule-based detection (trustabl scan) makes no network call — there is no LLM involved in the scan itself.trustabl enrichreads the scan output and calls the configured LLM provider (Anthropic, OpenAI, or Google Gemini) with BYOK (key stored viatrustabl llm key setat~/.config/trustabl/keys.json, mode 0600). Each call carries a request timeout, and--applyrewrites a file only when its current contents still match what the model reviewed (writing a.trustabl.bakbackup first) — a stale scan is skipped, never mis-applied. With--langsmith(opt-in, requiresLANGSMITH_API_KEY), tool-scope findings are additionally grounded in runtime trace evidence sampled from a LangSmith project: error rate, latency, and recent error messages for each flagged tool, carried on the output astrace_evidenceand fed to the LLM alongside the static code snippet; tools with no trace history degrade per finding to plain static enrichment. - Confidence scores are heuristic, not LLM-judged, and not yet calibrated against a labelled real-agent corpus — treat findings as signal to investigate.
- The CLI is the surface. No web app, API server, or hosted service:
pipe
--format jsonor--format sarifinto your own automation. On GitHub Actions,trustabl/trustabl-actionwraps the scan and uploads SARIF to the Security tab for you; for any other CI,--format sarif --output <file>produces a SARIF 2.1.0 report that feedsgithub/codeql-action/upload-sarifor any SARIF-aware step.
What it produces
Trustabl is a detect-and-report tool: it does not write or modify any
files in the scanned repo. Each run produces a ScanResult containing:
- Findings — one per rule hit, each with
severity,confidence, anexplanation, asuggested_fix, and the location it fired at (tool file/line, agent call site, subagent file, or the manifest). - Per-surface readiness scores (one per discovered tool, agent, subagent, or the repo as a whole) and an overall score (a breadth-aware, badness-weighted mean — weak surfaces pull it down harder, but a single poor surface does not zero it; the score is a triage signal, not the CI gate).
- The discovered inventory — tools, agents, hosted tools, MCP servers, subagents, skills, slash commands, plugin manifests, and Claude settings — surfaced at the top level for CI consumers.
The summary's tool surface, broken out
The human format honestly separates the three things people commonly conflate:
Tool definitions: 2 (custom tools with function bodies — scored below)
Agent tool grants: 14 (tool names the agent may call — audited by agent-scope rules)
Hosted tools: 1 (...)
Only the "Tool definitions" category flows through tool-scope rules (they have function bodies a rule can read). Agent grants and hosted instances are inputs to agent-scope rules, not unanalyzed — they just don't appear in the per-surface readiness table.
Output modes
--format human (default) renders a human summary to stdout and live
progress to stderr — an animated spinner and progress bar on an
interactive terminal, or plain [phase] summary lines when piped
(CI-friendly).
--format json marshals the full ScanResult for piping into your
own automation.
--format sarif emits a SARIF 2.1.0 document, suitable for
github/codeql-action/upload-sarif and other SARIF-aware tools. The suggested
fix is carried at the rule level (help.text); Trustabl emits no per-result
fixes[], so the document passes GitHub Code Scanning's schema validator (which
rejects a fix that lacks artifactChanges).
--json-out <file> and --sarif-out <file> write the JSON / SARIF document to a
file independent of --format — one scan can print the human summary to stdout
while persisting both machine artifacts. The file bytes are identical to the
matching --format stdout output.
--bom-out <file> additionally writes a byte-stable CycloneDX 1.5 BOM of the
dependencies the repo declares across every supported language — requirements.txt
/ pyproject.toml / Pipfile (pip), package.json (npm), go.mod (Go),
composer.json (Composer), *.csproj (NuGet), Cargo.toml (Cargo). It is pure
inventory of DECLARED direct deps and makes no network call. Where the manifest
carries a license field (package.json, Cargo.toml, composer.json,
pyproject.toml), the SPDX identifier is included in each component's
licenses[] array in the BOM.
--vuln-scan turns that BOM into a vulnerability verdict: it matches the repo's
concretely-pinned dependencies against a pinned OSV snapshot
and reports each affected package as a finding carrying the advisory ID
(CVE / GHSA / PYSEC / …), a CVSS-derived severity, and the first fixed version —
so a vulnerable dependency fails the scan through the normal severity gate and
exit codes and lands in the JSON / SARIF output alongside the rule findings, on
ScanResult.vulnerabilities. Unlike the rest of a scan it is opt-in and
online: the OSV snapshot is fetched from osv.dev on first use, cached under
your user cache directory, and then cache-first — a later --vuln-scan
reuses the cached database (no re-download) until it is older than 24h, so
repeated scans are fast and offline-capable. trustabl vulndb pull refreshes the
cache on demand; --no-rules-update pins to the cache at any age (fully offline).
Only concretely-pinned versions are matched
— a declared range (^1.0, >=2) can't be resolved to one version without a
lockfile, so it is left unmatched rather than guessed. The snapshot version is
folded into the ScanID only when --vuln-scan is on, so the result is honest
about which vulnerability data produced it while a default scan stays
byte-identical to before.
Combining --vuln-scan with --bom-out upgrades the CycloneDX document from a
plain inventory into a BOM plus VEX: the matched advisories are emitted as a
CycloneDX 1.5 vulnerabilities[] array — each with the advisory ID, an OSV
source, a severity rating, an upgrade recommendation, and an affects[]
reference linking it to the affected component's bom-ref — so a single
trustabl scan ./repo --vuln-scan --bom-out bom.json produces a standards-based
artifact that any CycloneDX-aware tool can ingest. Without --vuln-scan the
vulnerabilities[] array is omitted and the BOM stays pure inventory.
--format json and --format sarif are progress-silent and byte-stable
across identical-input runs (pure functions of the ScanResult). The human
format is not byte-stable by design: its ANSI color is auto-detected from the
terminal (TTY vs pipe, NO_COLOR), so the same scan can render with or without
color. Use --no-color, or diff the JSON/SARIF output, when byte-stability
matters.
Diagnostics (--verbose / --debug)
--verbose (-v) narrates the scan on stderr: rule provenance (repo, ref,
resolved SHA, and any cache fallback), per-phase discovery counts (languages,
tools, agents, detected SDKs, loaded detectors, unaudited SDKs), output
destinations, and a final result summary (scan ID, score, findings by severity,
exit code). --debug adds everything --verbose shows plus per-phase timing and
capped per-entity / per-finding detail (each discovered tool/agent and each
finding with its file:line).
Both are global flags — they work on scan, mcp, and rules pull, and may
appear before or after the subcommand (trustabl -v scan … or trustabl scan -v …). --debug implies --verbose. Both write only to stderr, so they never
perturb the report on stdout or the JSON/SARIF byte-stability contract:
--format json --debug still emits a clean document on stdout while the
diagnostics stream to stderr. Diagnostic color follows the same rules as the
report (off under --no-color, NO_COLOR, or when stderr is not a terminal).
Because an animated progress panel and interleaved log lines would corrupt each
other on the same stderr, --verbose/--debug automatically render progress as
plain [phase] lines instead of the live spinner.
Saving diagnostics to a file. There is no dedicated --log-file flag —
because diagnostics are a separate stream (stderr), redirecting stderr is the
intended mechanism:
# Report and diagnostics to separate files (stdout vs stderr)
trustabl scan ./repo --debug --format json >report.json 2>diagnostics.log
# Human report on screen, diagnostics to a file
trustabl scan ./repo --debug 2>diagnostics.log
# Everything (report + diagnostics) in one file
trustabl scan ./repo --debug &>everything.log
With --format json/sarif progress is off, so the stderr file is
diagnostics-only; with --format human it also carries the plain [phase]
progress lines.
Exit codes:
0— no findings ≥ medium severity (or no findings at all).1— at least one finding ≥ medium severity, OR--strictwith any finding present.2— scanner / I/O error, OR no usable rules found and none fetchable (runtrustabl rules pull), OR a signed channel (--rules-source) that failed verification (bad signature, untrusted/expired key, channel confusion, an expired or rolled-back statement, or a digest mismatch) — Trustabl refuses to run unverified rules.
OpenShell surfaces are still discovered (shell-invocation functions,
openshell/*.yaml policies) and reported on a Risk surfaces: openshell
block in the human format: the count of shell-invoking functions, the first
three file:line locations (deterministically sorted), a why: line stating
the threat model (a prompt-injected agent that exposes one of these as a
callable tool can run arbitrary commands), and a fix: line with concrete
remediations (sandbox, allowlist, drop shell=True, keep shell logic out
of agent-callable code). The OSH-* detection rules that audited these
surfaces have moved to a closed-source companion project; with no OSH rules
shipped, such repos fire no rule and no META finding — the block makes
the unaudited risk legible without claiming an audit happened. OpenShell is
a risk surface, not an SDK, so it is not flagged as "unaudited" the way an
unknown SDK would be.
Install
Homebrew (macOS, Linux)
brew install trustabl/tap/trustabl
Scoop (Windows)
scoop bucket add trustabl https://github.com/trustabl/scoop-bucket
scoop install trustabl
Docker
docker run --rm -v "$PWD:/repo" ghcr.io/trustabl/trustabl:latest scan /repo
Direct download
Grab a prebuilt archive for your platform from the
Releases page. Each release
includes a checksums.txt and a build-provenance attestation; verify with:
gh attestation verify <archive> --repo trustabl/trustabl
Attestation
Test Usage Example
Prerequisite and setup:
Install cosign (any 2.x or 3.x — Trustabl adapts to the installed version)
Add to target folder path:
export PATH="<folder_path>:$PATH"
Export and Generate Key Pair:
export COSIGN_PASSWORD=""
skip the passphrase prompt (omit for a real key)
cosign generate-key-pair
writes cosign.key (private) + cosign.pub (public)
Trustabl Scan and Attest:
./trustabl.exe scan https://github.com/google/adk-samples --json-out report.json --attest --attest-key cosign.key --attest-bundle att.bundle.json --attest-no-tlog
VERIFICATION
./trustabl.exe verify report.json --key cosign.pub --bundle att.bundle.json --no-tlog
Keyless (CI / no key to manage)
Keyless signs with your ambient OIDC identity instead of a key pair — nothing
to generate or store. In CI (GitHub Actions) the runner's identity is used
automatically; on a laptop it opens a browser to log in. Either way the
signing event is recorded in the public Rekor transparency log — so use key
mode above if that visibility is unacceptable. No --no-tlog here (keyless is
the transparency-log path), so the cosign v2/v3 difference does not apply.
Scan and Attest (keyless):
./trustabl.exe scan https://github.com/google/adk-python --json-out report.json --attest --attest-bundle att.bundle.json
VERIFICATION (keyless — you must pin who signed and the issuer):
./trustabl.exe verify report.json --bundle att.bundle.json --certificate-identity "<signer-identity>" --certificate-oidc-issuer "https://token.actions.githubusercontent.com"
In GitHub Actions the identity is the workflow ref, e.g.
https://github.com/OWNER/REPO/.github/workflows/scan.yml@refs/heads/main.
cosign (optional — only for scan attestation)
Trustabl's attest / verify commands and scan --attest shell out to the
cosign CLI —
cosign does the signing and verification, and Trustabl ships no keys of its own.
It is needed only if you use attestation; a plain scan never touches it.
# macOS / Linux
brew install cosign
# Windows
scoop install cosign
In CI you do not install it by hand — the Trustabl GitHub Action and GitLab
component add sigstore/cosign-installer for you.
Scan with attestation — step by step (key mode, offline)
Key mode signs with a local key pair and skips the public transparency log — no browser, no network, ideal for a laptop or air-gapped run.
cosign version: works with cosign 2.x and 3.x — Trustabl selects the right no-transparency-log mechanism per version automatically (
--tlog-upload=falseon v2; a no-Rekor--signing-configon v3+, which removed that flag). On a PATH install the command istrustabl; for a local Windows build it is.\trustabl.exe.
Attestation tiers: this is the free-tier basic attestation — it signs scan provenance (scan ID, rules version, reliability score, verdict, severity counts). Behavioral and policy-aware attestations are on the paid roadmap.
Step 0 — install cosign (see the commands above for your OS).
Step 1 — generate a signing key pair:
export COSIGN_PASSWORD="" # skip the passphrase prompt (omit for a real key)
cosign generate-key-pair # writes cosign.key (private) + cosign.pub (public)
Step 2 — scan and sign in one step:
trustabl scan https://github.com/google/adk-samples \
--json-out report.json \
--attest --attest-key cosign.key --attest-bundle att.bundle.json --attest-no-tlog
Writes report.json (the scan result), trustabl-predicate.json, and the signed
att.bundle.json. Exit 1 only means findings were present — the bundle is still
written (it signs the verdict, pass or fail).
Step 3 — verify (consumer side):
trustabl verify report.json --key cosign.pub --bundle att.bundle.json --no-tlog
Verified OK, exit 0 — the report is authentic and unmodified.
Step 4 — (optional) prove tamper detection:
echo '{"tampered":true}' >> report.json
trustabl verify report.json --key cosign.pub --bundle att.bundle.json --no-tlog
Verification now FAILS (exit 1): the signature is bound to the exact report bytes, so any edit is caught.
Keyless signing (no private key to manage) is the default in CI, where an ambient OIDC identity exists; it also records the signature in the public Rekor transparency log. See Use for the full command surface.
Claude Code plugin
The plugin installs two skills (trustabl-scan and trustabl-enrich), a
SessionStart hook, and a bundled MCP server — and auto-downloads the pinned
CLI binary on first session (no sudo, nothing outside Claude Code's plugin
data directory touched).
Install from within a Claude Code session using the /plugin slash command:
/plugin install trustabl@claude-plugins-official
Then reload to activate:
/reload-plugins
After installation, the mcp__trustabl__scan tool is available for direct
calls, the trustabl-scan and trustabl-enrich skills appear in the skill
picker, and the hook confirms the binary is ready at session start. Uninstall
with /plugin uninstall trustabl.
Claude Code agent
A standalone subagent (trustabl) runs the full scan → enrich → review →
apply pipeline interactively. It does not require the plugin — just the
trustabl binary already on your PATH. Copy the agent file into your project
or home config:
# Project-level (scoped to this repo)
curl -fsSL https://raw.githubusercontent.com/trustabl/trustabl/main/agents/trustabl.md \
-o .claude/agents/trustabl.md
# Global (available in every project)
curl -fsSL https://raw.githubusercontent.com/trustabl/trustabl/main/agents/trustabl.md \
-o ~/.claude/agents/trustabl.md
Once installed, invoke it via @trustabl in Claude Code or select it from
the subagent picker. It will ask which directory to scan, run trustabl scan
and trustabl enrich, then walk you through each proposed fix as a diff.
The enrich step requires an Anthropic API key. Set it before invoking the agent:
export ANTHROPIC_API_KEY=sk-ant-api03-... # or: trustabl llm key set
From source
Requires CGO_ENABLED=1 because the AST parsers use tree-sitter
(Python + TypeScript + TSX bindings), which is a C library:
# macOS / Linux
CGO_ENABLED=1 go build -o trustabl ./cmd/trustabl
# Cross-compile: pick a C toolchain for the target. zig is the easiest.
CGO_ENABLED=1 CC="zig cc -target x86_64-linux-gnu" \
GOOS=linux GOARCH=amd64 go build -o trustabl-linux ./cmd/trustabl
This is the cost of using tree-sitter for accurate AST parsing. If a
single-binary, no-CGO distribution becomes a hard requirement later, the
swap target is github.com/go-python/gpython for Python (with lower
fidelity on modern Python); TypeScript would need a separate replacement.
Use
# Local repo
trustabl scan ./path/to/agent-repo
# GitHub repo (shallow clone to temp dir, removed on exit)
trustabl scan https://github.com/org/repo
# Restrict detectors
trustabl scan ./repo --detectors claude_sdk
trustabl scan ./repo --detectors openai_sdk
trustabl scan ./repo --detectors google_adk
trustabl scan ./repo --detectors claude_sdk,openai_sdk,google_adk
# --detectors openshell is accepted but selects zero rules (pack is closed-source now)
# Agent Skill security (SKILL.md) — flags unrestricted allowed-tools (a bare
# `Bash` grant), pre-model dynamic-context exec, bundled-script network egress /
# secret reads, committed secrets, hidden-Unicode prompt injection, and a
# description that claims read-only while granting side-effecting tools (the
# CSKILL-* rules). Skills are discovered and scanned automatically.
trustabl scan ./repo # scans skills alongside tools/agents/MCP
trustabl scan ./repo --detectors claude_skill # only the Agent Skill (CSKILL-*) rules
trustabl scan ./path/to/my-skill # point straight at one skill's directory
# Dependency BOM (supply chain): export the repo's DECLARED deps across all
# supported languages (pip / npm / Go / Composer / NuGet / Cargo manifests) as a
# CycloneDX SBOM, to hand to OSV-Scanner / Dependabot / syft. Pure inventory —
# the scan itself does no CVE lookup.
trustabl scan ./repo --bom-out sbom.json
# Vulnerability scan (opt-in, online): match the repo's pinned deps against the
# OSV database and FAIL on known CVEs — advisory id, CVSS severity, fixed version.
trustabl vulndb pull # pre-download OSV (optional; --vuln-scan auto-fetches)
trustabl scan ./repo --vuln-scan # BOM inventory + CVE verdict in one pass
trustabl scan ./repo --vuln-scan --bom-out bom.json # CycloneDX BOM + VEX (vulnerabilities[]) in one file
# License scan (opt-in): flag dependencies with copyleft licenses (GPL-2.0,
# GPL-3.0, AGPL-3.0, LGPL-2.1, SSPL-1.0) as medium-severity findings.
# License data is read from the manifest at parse time — no network call.
trustabl scan ./repo --license-scan
trustabl scan ./repo --license-scan --bom-out bom.json # findings + BOM with licenses[]
# Secret scan (opt-in): walk all text files in the repo for hardcoded
# credential literals (SECRET-LIT-001, high) and scripts that read
# credential environment variables (SECRET-ENV-001, medium).
trustabl scan ./repo --secret-scan
# JSON output for CI piping
trustabl scan ./repo --format json
# SARIF output for GitHub Code Scanning / SARIF-aware tools
trustabl scan ./repo --format sarif > trustabl.sarif
# Write the report to a file instead of stdout (any format). --output writes
# the file even when the scan exits 1 on findings, so a CI step can upload it.
trustabl scan ./repo --format sarif --output trustabl.sarif
# One scan, both machine artifacts written to files (human summary to stdout)
trustabl scan ./repo --json-out trustabl.json --sarif-out trustabl.sarif
# Exit 1 on any finding regardless of severity
trustabl scan ./repo --strict
# --- Attestation (opt-in; requires the cosign CLI on PATH) -------------------
# Sign the JSON report into a cosign attestation of the SCANNED repo's result.
# Keyless by default: in CI it uses the runner's ambient OIDC identity (no keys
# to manage) and logs the signing event to the PUBLIC Rekor transparency log.
trustabl scan ./repo --json-out trustabl.json --attest
trustabl attest trustabl.json # same, as a separate step
# Offline / private signing with a key (no public transparency log):
trustabl attest trustabl.json --key cosign.key --no-tlog
# Verify (run where you CONSUME the attestation). Keyless pins who signed + issuer:
trustabl verify trustabl.json \
--certificate-identity https://github.com/OWNER/REPO/.github/workflows/scan.yml@refs/heads/main \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
trustabl verify trustabl.json --key cosign.pub --no-tlog # key-mode, offline
# Download / refresh the detection rule packs into the local cache
trustabl rules pull
# Validate a local rule-pack directory against this build's schema (CI gate
# for the trustabl-rules repo — strict-loads every pack, fails on the first error)
trustabl rules validate ./trustabl-rules
# Use a custom rules repo, or pin a specific released ruleset (env: TRUSTABL_RULES_REPO).
# Default pulls the latest reviewed rules from trustabl-rules main; pin a tag for stability.
trustabl scan ./repo --rules-repo https://github.com/org/my-rules
trustabl scan ./repo --rules-ref v0.1.0
# Air-gapped / offline: skip the network fetch, use the cached rules only
trustabl scan ./repo --no-rules-update
# Progress output (human format): animated on a terminal, plain lines when piped
trustabl scan ./repo # spinner + bars on a TTY; "[phase] summary" lines when piped
trustabl scan ./repo --no-progress # disable progress entirely
# Diagnostics on stderr (global flags; stdout/report unaffected)
trustabl scan ./repo --verbose # -v: rule provenance, discovery counts, result summary
trustabl scan ./repo --debug # + per-phase timing and per-entity/per-finding detail
trustabl scan ./repo --debug --format json > out.json # clean JSON on stdout, diagnostics on stderr
# Run as a stdio MCP server so an MCP client (Claude Code, Cursor, Claude
# Desktop) can scan code an agent just wrote (see "Run as an MCP server" below)
trustabl mcp
# Configure LLM provider, then enrich a scan result with AI explanations and fixes
export ANTHROPIC_API_KEY=sk-ant-api03-... # preferred: env var (works in CI, no setup)
trustabl llm list # show configured providers with masked keys
trustabl llm key set # prompt securely for an API key (persistent)
trustabl llm key set sk-ant-api03-... # set key non-interactively
trustabl llm key get # show masked key for active provider
trustabl llm key delete # delete key with confirmation prompt
trustabl llm model set claude-sonnet-4-6 # change model for active provider
trustabl llm provider set openai # switch active provider (auto-creates entry)
trustabl llm provider list
No comments yet
Be the first to share your take.