CLI (tc)
Hardened command-line client for ThreatCluster. Built for AI agents and humans. Install, authenticate, and query threat data from your terminal.
tc is the official ThreatCluster command-line client. It's designed primarily
for AI agents to shell out to, and for humans who'd rather pipe JSON through
jq than click around a UI.
The CLI is a thin wrapper over the public REST API: it doesn't add capabilities the API doesn't have, but it does add credential hygiene, bearer-token caching, scope downgrade for sub-processes, and structured error handling that you'd otherwise have to build yourself.
If you're integrating from a programming language other than Python (Go, JS,
Rust), call the API directly. If you're writing shell
scripts, AI agent tools, or working interactively, use tc.
Quick start
# 1. Install from PyPI
pipx install threatcluster-cli
# 2. Mint an agent key in the web UI
# Settings → CLI → "Mint key" → copy the tc_agent_… value once
# 3. Log in (paste the key when prompted)
tc auth login
# 4. Use it
tc threats list --limit 5 | jq -r '.threats[].ai_title'
tc search "Volt Typhoon" --limit 10
tc darkweb ransomware victims --group lockbit
Package on PyPI: https://pypi.org/project/threatcluster-cli/
Install
Recommended: pipx
pipx install threatcluster-cli
pipx installs tc into an isolated Python environment and puts it on your
PATH. If you don't have it: python3 -m pip install --user pipx && pipx ensurepath.
The package is at https://pypi.org/project/threatcluster-cli/ — pin a specific
version with pipx install threatcluster-cli==<x.y.z> if you need reproducibility.
If you already have a Linux tc (the kernel traffic-control tool) at
/usr/sbin/tc, make sure your PATH lists ~/.local/bin first:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
exec $SHELL
which tc # → ~/.local/bin/tc
One-off: uvx
uvx tc threats list --limit 5
Useful in CI or for trying without committing to an install.
From source
If you're hacking on the CLI itself, install from a checkout:
git clone https://github.com/Jam0k/threatcluster-cli
pipx install ./threatcluster-cli
# or, for an editable install while iterating:
pipx install --editable ./threatcluster-cli
Shell completion
tc --install-completion bash # or zsh, fish, powershell
exec $SHELL
tc thr<TAB> # → tc threats
Authentication
tc uses a two-tier credential model — see Agent keys & scopes
for the full design rationale.
- Refresh credential (
tc_agent_…or legacytc_live_…): long-lived, scoped, stored in your OS keyring. Never sent on a data request. - Bearer JWT: 15-minute token minted from the refresh credential. Sent on every API call. Cached in process memory only.
You only ever paste the refresh credential. The bearer is invisible.
tc auth login
tc auth login
Prompts you to paste a refresh credential (input hidden). Validates it by minting one bearer, then saves it.
Storage priority (CLI picks the first available):
TC_REFRESH_TOKENenv var — for CI; bypasses storage.- OS keyring — Keychain (macOS), SecretService (Linux desktop), Credential Manager (Windows). Encrypted at rest by the OS.
~/.config/tc-cli/credentials— 0600 file, current-uid only. Fallback when no keyring is available (headless Linux).
If the credential file's mode is anything other than 0600, or its owner isn't
the current user, tc refuses to load it.
tc auth status
tc auth status # short
tc auth status --verbose # full diagnostics
{
"api_url": "https://threatcluster.io",
"authenticated": true,
"bearer_expires_in": 900,
"key_id": "tk_e5fdefc9",
"org_id": null,
"org_name": null,
"scopes": ["darkweb:read", "entities:read", "feeds:read",
"iocs:read", "threats:read", "vulns:read"],
"storage": "keyring_or_file",
"token_prefix": "tc_agent_fDr…"
}
--verbose adds bearer_jti, bearer_session_id, bearer_max_requests,
and storage_detail (the storage backend class). Use it when something's
misconfigured.
tc auth logout
tc auth logout # hard kill: revokes server-side AND clears local
tc auth logout --keep-remote # local-only clear (you're moving the key)
Default behavior calls POST /api/auth/agent/self-revoke so the credential
dies everywhere — not just this machine.
Configuration
Environment variables (all optional):
| Variable | Default | Purpose |
|---|---|---|
TC_API_URL |
https://threatcluster.io |
Override API base. Plaintext http:// is rejected unless target is 127.0.0.1/localhost. |
TC_REFRESH_TOKEN |
— | Refresh credential. Overrides keyring + file. Intended for CI. |
TC_SCOPES |
(full set) | Comma-separated subset to request when minting a bearer. Cannot exceed the refresh credential's scopes. |
TC_SESSION_ID |
— | String identifier for per-session request budgeting. |
TC_MAX_REQUESTS |
— | Cap requests per TC_SESSION_ID; subsequent requests get 429. |
TC_PROPAGATE_AUTH |
0 |
If 1, propagate TC_REFRESH_TOKEN to subprocesses. Off by default for safety. |
TC_DEBUG |
0 |
If 1, print redacted requests to stderr. |
Output
All commands print JSON to stdout, errors to stderr.
tc iocs feed, tc iocs export, and tc threats stix may return plain text
(IOC-per-line, STIX bundles), in which case the body is printed as-is so you
can pipe it into grep, sort, or a SIEM ingest.
Commands
Top-level
| Command | Scope | Purpose |
|---|---|---|
tc auth login | status | logout |
— | Manage local credential |
tc search <query> |
entities:read + threats:read |
Smart router across entities + threats |
tc threats {list,get,iocs,stix} |
threats:read / iocs:read |
Threat clusters and their IOCs |
tc iocs {feed,export} |
iocs:read |
Indicator feed and bulk export |
tc entities {search,get,related,trending} |
entities:read |
Actors, malware, tools |
tc vulns {list,get} |
vulns:read |
CVE list and detail |
tc darkweb ransomware victims |
darkweb:read |
Ransomware leak-site victims |
tc darkweb breaches |
darkweb:read |
Dark-web breach data |
tc darkweb keyword-hits |
darkweb:read |
Dark-web keyword matches (Business tier) |
tc feeds {list,get} |
feeds:read |
Your custom intel feeds |
tc cluster open <id> |
— | Print + open a cluster's web URL |
tc auth and tc cluster are local — they don't hit the API for data.
Stdin chaining (-)
Any command that takes a positional id/identifier accepts the literal -
to read whitespace-separated ids from stdin. Useful for shell composition:
# IOCs for the top 5 trending threats
tc threats list --limit 5 \
| jq -r '.threats[].cluster_id' \
| tc threats iocs -
# STIX export of every "ransomware" cluster
tc threats list --query ransomware --limit 50 \
| jq -r '.threats[].cluster_id' \
| tc threats stix -
# CVE detail for everything in the past 24h
tc vulns list --since 24h \
| jq -r '.cves[].cve_id' \
| tc vulns get -
--watch (live feeds)
tc threats list, tc darkweb ransomware victims, tc darkweb breaches, and
tc vulns list all support --watch. Each polls the endpoint on an interval,
suppresses items it's already seen, and emits only new items as NDJSON
(one JSON document per line). Ctrl+C to stop.
# Live ransomware feed into Slack
tc darkweb ransomware victims --watch --interval 60 \
| while read line; do
msg=$(echo "$line" | jq -r '"new victim: \(.group): \(.name)"')
curl -s -X POST -d "{\"text\":\"$msg\"}" "$SLACK_WEBHOOK_URL"
done
# Live CVE feed (slow-moving, 2-min default interval)
tc vulns list --watch --severity CRITICAL
# Watch threats matching a keyword
tc threats list --query lockbit --watch --interval 30
--interval is in seconds. Defaults: 60s for most commands, 120s for vulns.
The first poll seeds the dedupe set silently; subsequent polls emit only items
not seen yet. So if you start --watch and there's nothing new, you see nothing.
tc search (smart router)
tc search <query> [--limit N] [--only entities|threats]
Calls both /entities/search and /threats?keyword=…, merges results into a
common shape:
{
"query": "Volt Typhoon",
"count": 5,
"results": [
{"kind": "entity", "type": "apt_group", "value": "Volt Typhoon", "score": 4, "raw": {...}},
{"kind": "threat", "id": "1796fc13-…", "title": "…", "score": 87.2, "summary": "…"}
]
}
Use --only entities if you want to disambiguate a name; --only threats if
you want news/coverage only.
Other commands
Each subcommand has full --help:
tc threats list --help
tc darkweb ransomware --help
tc cluster open --help
Cookbook
Per-session containment for sub-agents
See Containment for agent loops below — the
same TC_SCOPES + TC_SESSION_ID + TC_MAX_REQUESTS pattern is how you
narrow a bearer for a subprocess.
Use in CI
GitHub Actions and GitLab CI can authenticate via OIDC instead of a long-lived secret. The CLI auto-detects CI; from your workflow:
- name: Query ThreatCluster
run: |
pipx install threatcluster-cli
tc threats list --query "$KEYWORD" --limit 50 > threats.json
env:
TC_API_URL: https://threatcluster.io
# OIDC handled automatically by the CLI; no secret in env
CI OIDC requires the server to be configured with allowlists for your repo — see Agent keys & scopes.
Pivoting between terminal and web
# Open a cluster in your browser
tc threats list --query "lockbit" --limit 1 \
| jq -r '.threats[].cluster_id' \
| tc cluster open -
# Or just print the URL
tc cluster open <cluster-id> --no-browser
tc cluster open accepts either a full cluster UUID or its 8-char short_id —
full UUIDs are shortened automatically (last 8 hex chars, dashes stripped).
The resulting URL points at https://threatcluster.io/cluster/<short_id>.
Bulk IOC export for SIEM
# Bulk JSON with confidence + reason metadata, last 7 days
tc iocs export --format json --hours 168 > tc-iocs.json
# Plain-text feeds, one IOC per line — drop into a blocklist
tc iocs feed --type ip > ip-blocklist.txt
tc iocs feed --type hash > hash-feed.txt
# For a STIX 2.1 bundle, export per-cluster instead:
tc threats stix <cluster-id> > cluster.stix
tc iocs feed returns plaintext (one IOC per line) — drop straight into a
firewall rule, SIEM blocklist, or grep. --type accepts all, ip,
domain, hash, or email. --hours defaults to 720 (30 days).
tc iocs export does not produce STIX; use tc threats stix per cluster.
Use with AI agents
tc is the supported way to give an AI agent access to ThreatCluster.
The CLI was designed for this case from the start: every command emits clean JSON, errors go to stderr with non-zero exit codes, scopes can be downgraded per task, and per-session request budgets cap blast radius if the agent goes sideways. There is no separate MCP server — the CLI is the agent surface.
Claude Code (and any shell-running agent)
The simplest path. Claude Code, Cursor, Aider, and any agent that can run a
shell command can use tc directly. Authenticate once on the machine, then
let the agent invoke commands as needed:
# One-time setup on the agent's host
pipx install threatcluster-cli
tc auth login # paste a refresh credential
# The agent now runs commands like any other tool
tc threats list --query "lockbit" --limit 10
tc darkweb ransomware victims --days 1
tc iocs feed --type hash
If you want the agent to discover what's available, point it at this page or
let it run tc --help, tc threats --help, etc. — the help text is short
and machine-readable.
Mint a per-agent key
Don't share a personal credential with an agent. Mint a dedicated one:
- Settings → CLI → Mint key
- Tick only the scopes the agent needs (e.g.
threats:read,iocs:read, leavedarkweb:readoff if not needed). - Give the key a name that identifies the agent (
claude-code-soc-bot). - On the agent's host:
TC_REFRESH_TOKEN=tc_agent_… tc auth login, or paste it intotc auth loginmanually. - If the key leaks, revoke it from the same Settings page — the agent stops working within ~15 minutes (bearer TTL).
See Agent keys & scopes for the full scope reference.
Containment for agent loops
The two env vars below cap what a compromised or runaway agent can do, even if it has a valid credential. Use both in CI and in any long-lived agent:
# Per-task scope downgrade — child cannot escalate
TC_SCOPES=threats:read,iocs:read \
TC_SESSION_ID="agent-task-$(uuidgen)" \
TC_MAX_REQUESTS=100 \
tc threats list --limit 5
What this gets you, in order:
- Scope downgrade: bearer is minted with the subset of scopes you specify.
The server rejects scopes the parent credential doesn't have, so the agent
cannot upgrade itself to
admin:write. - Session budget: the next 100 requests under this session id will succeed, the 101st gets HTTP 429. Caps the damage of a tight loop or prompt-injection scrape.
- 15-minute bearer TTL: even without the budget, a stolen bearer dies fast.
Wrapping tc as a custom tool
If your agent framework expects "tools" with typed schemas (Claude API tool
use, OpenAI function calling, LangChain), wrap CLI calls into one tool per
intent — don't expose raw tc argv to the model. A small Python adapter:
import json, subprocess
def search_threats(query: str, limit: int = 10) -> dict:
"""Search ThreatCluster for clusters matching `query`."""
out = subprocess.run(
["tc", "search", query, "--limit", str(limit)],
capture_output=True, text=True, check=True,
env={"TC_SCOPES": "threats:read,entities:read",
"TC_MAX_REQUESTS": "20",
"TC_SESSION_ID": f"search-{query[:20]}",
**dict(__import__("os").environ)},
)
return json.loads(out.stdout)
Each tool sets its own TC_SCOPES minimum and a small TC_MAX_REQUESTS cap.
The model never picks scopes — your code does.
MCP / Claude Desktop
ThreatCluster previously shipped a hosted MCP server at /mcp. It has been
retired; agents now use this CLI instead. If you specifically need MCP for
Claude Desktop, you can build a thin local MCP server that wraps tc calls
(the mcp Python SDK
takes ~50 lines for this). We don't ship one in-tree — if there's demand,
open an issue.
Security model
Designed for the case where an AI agent shells out to tc. Threats considered:
- Credential theft from disk: keyring is encrypted by the OS; the file fallback is 0600 + uid-checked.
- Credential theft from process table:
--api-keyflag is rejected — argv leaks via/proc/<pid>/cmdline. Pass via env or storage only. - Credential leak via subprocess inheritance:
TC_REFRESH_TOKENis scrubbed from child env unlessTC_PROPAGATE_AUTH=1is set explicitly. - Credential leak via logs: the HTTP client strips
AuthorizationandX-API-Keyfrom any debug log output (verified by a unit test). - Plaintext interception:
http://URLs are refused unless the host is127.0.0.1orlocalhost. - Prompt-injection scraping: a compromised agent with a valid bearer can
still scrape data within its scopes. Mitigations:
- Mint scoped bearers per task:
TC_SCOPES=threats:read tc …. - Cap per-session requests:
TC_MAX_REQUESTS=50 TC_SESSION_ID=foo tc …. - 15-minute bearer ttl bounds damage even without a budget.
- Mint scoped bearers per task:
Troubleshooting
could not reach <url>: ConnectError: Name or service not known
TC_API_URL is unset or wrong. Default is https://threatcluster.io.
export TC_API_URL=http://localhost:8000 # local dev
export TC_API_URL=https://threatcluster.io # prod (default)
refusing plaintext HTTP to non-loopback host
You set TC_API_URL=http://something.com. Plaintext is only allowed to
loopback. Use https:// or set the URL to http://127.0.0.1:….
<file> has insecure mode 0o644; run chmod 0600 on it or delete and re-login
The credential file's permissions widened (often by another tool). Either:
chmod 0600 ~/.config/tc-cli/credentials
# or
tc auth logout && tc auth login
HTTP 403: insufficient_scope
Your key was minted without the scope this command needs. Mint a new key in
Settings → CLI with the required scope ticked, or call tc auth login to
replace your local credential.
HTTP 403: scope_elevation_rejected
You set TC_SCOPES=foo:read where foo:read isn't on your refresh credential.
Either remove the env var or mint a new key with foo:read included.
HTTP 429: session_budget_exceeded
You hit the TC_MAX_REQUESTS cap for this TC_SESSION_ID. Mint a new bearer
(any new tc invocation will do, with a fresh TC_SESSION_ID or none).
HTTP 401: bearer expired
Bearers live 15 minutes. The CLI refreshes them automatically; if you see this error, the clock skew between client and server is >30 seconds. Sync NTP.
tc runs the kernel tc (Linux traffic control)
which -a tc
# /usr/sbin/tc ← kernel tool (wrong)
# /home/james/.local/bin/tc ← ours
Reorder PATH so ~/.local/bin is first:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
exec $SHELL
Versioning
threatcluster-cli follows SemVer. Major-version bumps
will be called out in cli/CHANGELOG.md and announced in release notes. Minor
and patch versions are backward-compatible.
The CLI talks to a stable URL prefix (/api/public/v1/*); breaking server
changes get a new prefix, not a quiet schema bump.
See also
threatcluster-clion PyPI — install source and version history- API Reference — the underlying REST endpoints
- Agent keys & scopes — credential design and scope reference
- IOC exports — STIX, CSV, and JSON export formats
- Custom feeds — the
feedsfamily of commands operates on these