# Analytics & Reporting
Source: https://docs.getunbound.ai/analytics
Understand agent activity, developer productivity, and AI usage across your organization
The Unbound dashboard provides analytics at several levels — from organization-wide summaries down to a single task trace, plus a full inventory of the AI tools running across your fleet. This page covers the main views and what each one is for.
Analytics answers *what your agents did*. [AI Tools Discovery](#ai-tools-discovery) answers *what your developers have installed*. Most teams use both.
## Analytics
**Dashboard:** [gateway.getunbound.ai/analytics](https://gateway.getunbound.ai/analytics)
### Overview
Organization-wide spend and activity at a glance — cost and token consumption over time, how spend splits across models, and response latency. Start here to see the shape of your AI usage before drilling into any one team or developer.
### Tasks
Each agent task that has finished processing, classified by the kind of work it was doing, with cost and token consumption attached. Shows spend by work type, so you can answer "what are we actually buying with our AI budget" — refactoring, tests, debugging, new features. Tasks are placed on the timeline by when they *started*, so one still running or still being classified won't be listed yet.
### Agentic Use
What your agents *did*, split into three views:
* **Terminal Run** — the shell commands agents executed, with a risk level on each.
* **MCP Actions** — the MCP servers and tools agents called, with risk categorization and per-user and per-server breakdowns.
Each server row carries a **Raw logs** action. It opens the full call history for that one server: every MCP tool call with its tool name, the developer who made it, and when. Expand a call to see the exact **arguments** sent and the **output** returned. Narrow it with the **Tool** filter.
Large values are handled in two places, and the page tells you which applies. A call whose arguments or output were too big to store in full shows a note with the original size and the stored preview. Anything over 4,000 characters is trimmed on screen with a **Show full** link — nothing is hidden, it just stops a multi-megabyte response freezing the page.
Raw logs may not be enabled for your organization; where it is, it's visible to the same roles that can see Agentic Use.
* **Skills** — which Agent Skills were actually run, by whom, and through which agent. See [Skills](#skills) below.
Each action is attributed to a user and session, and carries how it was initiated — user-intentional, agent-initiated, or unknown when there isn't enough signal to tell. Attribution is applied shortly after the action lands, so the newest rows can show as unclassified for a short while. This is the fastest way to see where a [Tool Policy](/policies/tool-policies) would have the most impact — watch the traffic here first, then write the rule.
Terminal Run, MCP Actions, unsanctioned MCP usage and the MCP server list can each be exported.
### Code Stats
How much of your codebase your agents are actually writing, broken down by tool and by model. Coverage is wider than it used to be — Claude Code, Claude Cowork, Cursor, Codex, GitHub Copilot and Augment Code all report the file edits their agents make. Shows your top code generators and the files and functionality most affected.
Code Stats is visible to admins, managers, and analytics viewers.
### Data
Every time a [Security Policy](/policies/security-policies) guardrail fires — sensitive data like PII or a secret detected in a prompt, and whether it was audited or blocked — it lands here. Two views:
* **Summary** — *Top Tools with Guardrail Actions*: which AI tools are triggering the most guardrail activity, as a table of tool, guardrail, action, and hit count. Start here to see where sensitive data is concentrated.
* **Logs** — the individual events behind those counts, each with its timestamp, tool, user, the guardrails that fired, and the action taken. Filter by tool type, guardrail, or action, and open any event for the full detail.
Click a row in Summary to drop straight into Logs pre-filtered to that tool, guardrail, and action — the fast path from *which tool is leaking* to *show me exactly what it sent*. Go back to Summary and that drill-down clears itself, while any filters you set by hand stay put.
This is the observability side of [Security Policies](/policies/security-policies): the policies decide what to catch, and this tab shows you what they caught.
The Data tab is a gated feature. If you don't see it in your dashboard, contact [support@unboundsecurity.ai](mailto:support@unboundsecurity.ai) to enable it for your organization.
### Users
Per-developer activity, oriented around how *agentic* each developer's workflow is: how long their agents run unattended, median run length, clean-completion rate, how many tasks they worked on, tokens, cost, cache-hit rate, and how many distinct models they used. Open a developer to see which models they actually leaned on and what share of their spend each one took.
Use it to see which developers are getting real leverage from agents, and which are still babysitting every step. Clean-completion rate and median run length stay blank until a developer has enough agent runs in the window for the number to mean something — a quiet week reads as blank, not as zero.
The same view ranks developers by consumption. **Power Users** and **Light Users** are the same list sorted highest-first and lowest-first, so you can read either end of the distribution; **Top Experimenters** is a separate list of the five developers who reached for the widest range of models.
This view may not be enabled for your organization. Contact [support@unboundsecurity.ai](mailto:support@unboundsecurity.ai) if you don't see it.
## Task Logs
**Dashboard:** [gateway.getunbound.ai/logs](https://gateway.getunbound.ai/logs)
Every agent session, logged with full prompt-level detail — messages sent and received, models used, token counts, tool calls made, and policy decisions. Filter by user, model, tool type, or date range, then click any task to trace the whole conversation end to end.
This is where you go when you need to answer "what exactly happened here."
## AI Tools Discovery
**Dashboard:** [gateway.getunbound.ai/ai-tools-discovery](https://gateway.getunbound.ai/ai-tools-discovery)
Analytics shows you the agents routed through Unbound. Discovery shows you **everything installed across your fleet** — including the tools nobody told you about. It's powered by [`unbound discover`](/cli/discovery).
### Summary
The fleet at a glance: how many AI tools are in use, how they're distributed across your org, and which users carry the most risk. From here you can **sanction** the agents you approve of — everything else is surfaced as unsanctioned, which is how you find Shadow AI.
### Inventory
The full catalogue of what Discovery found, in four views:
| View | What it shows |
| ---------------- | ----------------------------------------------------------- |
| **Tools** | Every AI coding agent installed, and who has it |
| **MCP Servers** | Every MCP server your developers have connected |
| **Skills** | The skills agents have picked up to extend what they can do |
| **Config Files** | The rules and instruction files steering each agent |
The **Skills** inventory here lists the skill files Discovery *found installed*. To see which skills are actually being *run* — invocation counts, trends, and who's using them — see [Skills](#skills).
### Users
Discovery from the people side — what each developer has installed, rather than what's deployed across the fleet. Open any user for a per-user risk view: their tools, their risk factors, and what's driving their score.
### Permissions
What your agents are permitted to do on each machine, with recommendations on the permissions worth denying.
Unsanctioned tools can be pushed to your SIEM as they appear — see [Webhooks](/integrations/webhooks) for the Shadow AI events.
## Skills
**Dashboard:** [gateway.getunbound.ai/analytics/agentic-use?tab=skills](https://gateway.getunbound.ai/analytics/agentic-use?tab=skills) — a sub-tab of **Analytics → Agentic Use**, not a page of its own.
Which Agent Skills your developers are actually *running* — how often, by whom, and through which agent. [AI Tools Discovery](#ai-tools-discovery) tells you which skills are installed across your fleet; this tells you which ones get used. Start here to see which skills have caught on, which sit unused, and who's leaning on them.
Headline numbers up top — total invocations, distinct skills used, and how many developers used them over the period — then a daily invocation trend, your most-invoked skills, and a breakdown of which agent (Claude Code, Cursor, and the rest) each skill was called from. Filter the whole view by skill, tool, user, or time range.
The **All Skills** table lists every skill with its invocation count, unique users, the tools that ran it, a risk level, and when it was last used. Open any skill to see its usage over time, its top users, and the rendered **SKILL.md** content itself — including each distinct version when several copies of a skill share one name across your fleet.
Skills is visible to admins only, and is gated. If you don't see the sub-tab, contact [support@unboundsecurity.ai](mailto:support@unboundsecurity.ai) to enable it for your organization.
## Insights
**Dashboard:** [gateway.getunbound.ai/insights](https://gateway.getunbound.ai/insights)
Insights is in **beta** and gated. If you don't see it in your dashboard, contact [support@unboundsecurity.ai](mailto:support@unboundsecurity.ai) to enable it for your organization.
How well your developers are actually *prompting* their agents — and where they're spending tokens on prompts that could be tighter. Where [Analytics](#analytics) measures usage and cost, Insights looks at prompt quality: it scores each developer's prompting efficiency, flags recurring weak spots, and suggests concrete ways to improve.
The main view ranks your users by an **efficiency score**, alongside their task count, API calls, tool types, cost, and the categories of weak prompting showing up in their work. A **Top Recommendations** panel pulls out the highest-impact fixes across the org. Open any user to drill into their individual tasks — each with its own efficiency score, cost, and summary — to see which prompts are dragging the score down.
## Custom Reports
**Dashboard:** [gateway.getunbound.ai/custom-reports](https://gateway.getunbound.ai/custom-reports)
Query your Unbound data in natural language, or with a structured builder. Example queries:
* "Which users ran the most delete commands this week?"
* "What models did my team use most in April?"
* "Show me all policy matches grouped by command family"
Queries can be saved, shared with your team, and assembled into **Dashboards** — reusable boards of the reports your team checks regularly.
## Billing & Usage Export
For cost-focused reporting, see [Cost Policies](/policies/cost-policies). Admins can also export a full usage report with daily granularity — broken down by user and model — from **Billing → Usage → Export** in the dashboard.
Configure security guardrails for AI tools
Scan devices and build your AI tool inventory
# Analytics
Source: https://docs.getunbound.ai/cli/analytics
Read Tool Use data from the terminal — terminal commands and MCP tool calls
`unbound analytics tool-use` reads the Analytics → Tool Use tables from the terminal: what agents ran, which MCP tools they called, and which policies matched.
Requires **Admin or Analytics Viewer** role. Run `unbound status` to check.
## Datasets
| Dataset | What it holds |
| -------------- | ----------------------------------------------------------------------------- |
| `terminal` | Terminal commands agents ran, with risk score and matched tool policies |
| `mcp-actions` | Individual MCP tool calls, with server, tool, targets and risk score |
| `unsanctioned` | MCP calls to servers your organization has not sanctioned (blocked or warned) |
| `mcp-servers` | One row per MCP server: action count and average risk |
```bash theme={null}
unbound analytics tool-use terminal
unbound analytics tool-use mcp-actions --server claude_ai_notion
unbound analytics tool-use unsanctioned --time-window THIRTY_DAYS
unbound analytics tool-use mcp-servers
```
Run `mcp-servers` first when you need a value for `--server` — it lists the names the other datasets accept.
## Filters
All of these take comma-separated values and work on every dataset unless noted.
| Flag | Filters by |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `--time-window ` | `ONE_DAY`, `THREE_DAYS`, `SEVEN_DAYS`, `FOURTEEN_DAYS`, `THIRTY_DAYS`, `THREE_MONTHS`. Default `THIRTY_DAYS` |
| `--start`, `--end` | A custom range as `YYYY-MM-DD`, up to 90 days. Not combined with `--time-window` |
| `--tool ` | AI tool, e.g. `CLAUDE_CODE,CURSOR,CODEX` |
| `--user ` | User IDs (see `unbound users list`) |
| `--risk ` | `Low`, `Medium`, `High` |
| `--policy ` | Tool policy IDs (see `unbound policy tool list`) |
| `--family ` | Command family — `terminal` only |
| `--server ` | MCP server or service name — MCP datasets only |
| `--initiator ` | `user`, `agent`, or `unknown` — **`terminal` and `mcp-actions` only**; the other datasets reject it with `unknown option` |
```bash theme={null}
unbound analytics tool-use terminal --risk High --time-window SEVEN_DAYS
unbound analytics tool-use terminal --family git_action,delete_file --initiator agent
unbound analytics tool-use terminal --start 2026-07-01 --end 2026-07-31
```
## Output
The default is a table sized to your terminal, showing a curated set of columns.
| Flag | Behavior |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--format ` | `table` (default), `json`, or `csv` |
| `--json` | Shorthand for `--format json` |
| `--wide` | Print full values instead of clipping to the terminal width |
| `--limit ` | Rows to show — default 20, max 100 for a table and 1,000 for JSON. Over the cap the command **exits 1 rather than clamping** |
| `--offset ` | Skip n rows, for paging |
| `-o, --output ` | Write to a file instead of stdout. Requires `--format json` or `--format csv` — with the default table it exits 1 with `-o needs --format json or --format csv` |
The table hides the widest columns to stay readable. Use `--wide`, or `--format json`, to see all of them.
`--limit` is capped lower for the table than for JSON because a table is read by a person and JSON is parsed by a program. Use `--format csv` for everything, rather than paging a table.
## Paging
Each page prints where it sits in the result set:
```
ℹ Showing 1-20 of 548. Page with --offset 20, or use --format csv to export every row.
```
Pass that offset to get the next page. Page with `next_offset` from the JSON envelope rather than adding `offset + returned_count`: the server counts source rows and drops some while serializing, so the two differ.
## For scripts and agents
`--format json` is a stable contract:
```json theme={null}
{
"dataset": "terminal-runs",
"columns": ["Timestamp", "User", "..."],
"rows": [["2026-08-05T09:55:44Z", "..."]],
"total_count": 1431,
"returned_count": 20,
"offset": 0,
"next_offset": 20,
"has_more": true
}
```
Every cell is a string, and `rows` align to `columns`. Page with `--offset next_offset` until `has_more` is `false`.
Diagnostics go to stderr and stdout carries only the payload, so it is safe to pipe. The exit code is `0` on success and `1` on any error.
```bash theme={null}
unbound analytics tool-use terminal --format json --limit 100 \
| jq -r '.rows[] | @tsv'
unbound analytics tool-use terminal --format json \
| jq -r '(.columns, .rows[]) | @csv'
```
## Exporting
`--format csv` exports every matching row rather than one page. The file is prepared in the background and the command waits for it — see [Exports](/cli/exports) for how that works, and for exporting Logs, Tasks and Users the same way.
```bash theme={null}
unbound analytics tool-use terminal --start 2026-07-01 --end 2026-07-31 \
--format csv -o july-terminal-runs.csv
```
Requires CLI **1.15.0** or later.
# Authentication
Source: https://docs.getunbound.ai/cli/authentication
Login, logout, and configure tenant URLs for the Unbound CLI
## Login
**Browser (recommended):**
```bash theme={null}
unbound login
```
**API key** — for CI/CD or headless environments:
```bash theme={null}
unbound login --api-key
```
**Custom domain:**
```bash theme={null}
unbound login --domain custom.example.com
```
Credentials are stored at `~/.unbound/config.json`.
## Logout
```bash theme={null}
unbound logout
```
Removes stored credentials while preserving custom URL settings.
## CLI status
```bash theme={null}
unbound status
```
Shows your email, organization, **role** (Admin/Manager/Member/Analytics Viewer), config file location, login state, API connectivity, and the **connected tools** wired through Unbound on this device (with their mode). Useful for debugging connection issues.
For a deep per-tool health check, run [`unbound doctor`](/cli/doctor).
### Role access
| Role | Data scope | Access |
| ---------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Admin | Organization | Full access to organization data, policies, users, settings, billing, integrations, and credentials. |
| Analytics Viewer | Organization | View dashboards, analytics, logs, policies, AI Tools Discovery, users, and cost data. Run analytics queries and CSV exports. Manage saved queries and custom dashboards. Cannot change organization controls or access credentials. |
| Manager | Self and direct or indirect reportees | View dashboards, analytics, logs, and cost data for their team. Cannot use organization administration controls. |
| Member | Self | Create and access their own applications and related data. Cannot view other users' analytics or logs. |
Admin, Analytics Viewer, and Member are assigned roles. Unbound grants Manager access when at least one user reports to that person.
***
## Tenant / Custom Domains
If your organization runs Unbound on its own hosts — for example `api.acme.com` instead of `api.getunbound.ai` — pass all three URLs at login. They persist to `~/.unbound/config.json`:
```bash theme={null}
unbound login --api-key \
--gateway-url https://api.acme.com \
--frontend-url https://gateway.acme.com \
--backend-url https://backend.acme.com
```
Or set them at any time without re-logging in:
```bash theme={null}
# All three at once:
unbound config urls https://api.acme.com https://gateway.acme.com https://backend.acme.com
# One at a time:
unbound config set-gateway-url https://api.acme.com
unbound config set-frontend-url https://gateway.acme.com
unbound config set-backend-url https://backend.acme.com
```
View current values:
```bash theme={null}
unbound config show
unbound config show --json
```
### Defaults
| Host | Default |
| -------------------- | ------------------------------- |
| Gateway (LLM proxy) | `https://api.getunbound.ai` |
| Frontend (dashboard) | `https://gateway.getunbound.ai` |
| Backend (CLI API) | `https://backend.getunbound.ai` |
# Chat
Source: https://docs.getunbound.ai/cli/chat
Query your Unbound usage data with natural language from the terminal
`unbound chat` lets you ask questions about your AI usage data in natural language. It renders charts and tables directly in the terminal.
Requires **Admin, Manager, or Analytics Viewer** role. Run `unbound status` to check.
## Conversations are two-sided
The assistant can answer, and it can ask. A question back is a normal reply, not an error — the turn succeeds and exits `0`, and you answer it in the next turn.
Every turn belongs to a conversation, and each one builds on the ones before it, so you can refine an answer instead of restating it. There are two ways to hold one:
* **Interactive session** — context is kept for you.
* **One-shot commands** — pass the conversation id back with `-c`.
## Interactive session
Start a multi-turn REPL:
```bash theme={null}
unbound chat
```
Session commands:
| Command | Action |
| ---------------- | ----------------------------------------------------------------------------- |
| `/help` | Show available commands |
| `/json` | Print the last result as JSON |
| `/export ` | Write the last result to a file, as the JSON transcript. Refuses to overwrite |
| `/clear` | Reset conversation history in memory |
| `/exit` | Quit (Ctrl+C also works) |
## One-shot mode
Ask a single question and exit:
```bash theme={null}
unbound chat -m "show cost by provider for the last 30 days"
unbound chat -m "which users spent the most this month"
unbound chat -m "MCP tool calls by server last week"
```
Each `-m` is its own process and sends no history of its own.
### Continuing a conversation
A reply that produced an answer ends with the command to continue it:
```
● Here's total cost by provider for the last 7 days...
continue: unbound chat -m "..." -c
```
Pass that id back with `-c` and the turns are read from the server, so the follow-up knows what came before:
```bash theme={null}
unbound chat -m "now just the top 2" -c
```
This is also how you answer a question the assistant asked you.
With `--json` the id is in the payload as `conversation_id`. The same `continue:` line is printed to your terminal on stderr, so it stays readable without entering the stream — it is skipped when stderr is redirected, since a script reads the id off the payload:
```bash theme={null}
id=$(unbound chat -m "cost by provider last 7 days" --json | jq -r .conversation_id)
unbound chat -m "now just the top 2" -c "$id"
```
A conversation id that matches nothing is not an error — the question is still answered, just without the earlier context. The reply says so on stderr and sets `resumed` to `false`.
A single turn can answer several things at once, and will say plainly what it could not do. Splitting a broad request across turns with `-c` tends to give sharper answers though, and is easier to check. Messages are capped at 4,000 characters.
## Output flags
| Flag | Behavior |
| --------------------- | ------------------------------------------------------------------------- |
| `--json` | Write the JSON transcript to stdout |
| `-o ` | Write the JSON transcript to a file (refuses to overwrite existing files) |
| `-c, --continue ` | Continue the conversation with this id |
```bash theme={null}
# Pipe to jq:
unbound chat -m "cost by provider last 30 days" --json \
| jq -r '.blocks[] | select(.type=="text") | .text'
unbound chat -m "cost by provider last 30 days" --json \
| jq '.blocks[] | select(.type=="chart") | .rows'
# Save to file:
unbound chat -m "cost by provider last 30 days" -o report.json
# Pipe and archive simultaneously:
unbound chat -m "cost by provider last 30 days" --json -o report.json | jq '.blocks | length'
```
With `--json`, stdout carries the transcript and nothing else — confirmations and hints go to stderr so the stream stays parseable. Requires CLI **1.13.0** or later.
### What the CLI produces
JSON, and charts rendered in the terminal. `--json`, `-o` and `/export` all write the same transcript, with chart data as `columns` and `rows` — ready to turn into a CSV yourself.
The CLI does not produce CSV or PNG files; those are download buttons in the web UI. PowerPoint, Slides and PDF are not available anywhere. If the assistant mentions CSV or PNG in its reply, it is describing the web UI, not this command.
## JSON response shape
```json theme={null}
{
"success": true,
"error": null,
"conversation_id": "",
"continue_command": "unbound chat -m \"...\" -c ",
"log_id": "",
"truncated": false,
"resumed": false,
"blocks": []
}
```
| Field | Meaning |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `success` | `false` when the turn failed |
| `error` | The reason when `success` is `false`, otherwise `null` |
| `conversation_id` | The conversation this turn belongs to. Pass it to `-c` to continue |
| `continue_command` | The next command spelled out, for a reader that should act on it rather than build one from `conversation_id`. `null` when the turn produced nothing |
| `log_id` | Identifies the stored turn, for feedback and for support to trace |
| `truncated` | The agent ran out of budget partway. The blocks present are real — ask a follow-up for the rest |
| `resumed` | Whether `-c` found the conversation. `false` means the turn ran without the earlier context |
| `blocks` | The answer, in the order it was produced |
A turn is an ordered transcript, so one question can return several charts with prose between them. Each block is either text or a chart:
```json theme={null}
{ "type": "text", "text": "Here's total cost broken down by provider..." }
```
```json theme={null}
{
"type": "chart",
"title": "Cost by Provider (Last 30 Days)",
"chart_type": "bar",
"row_count": 7,
"columns": ["dimension", "metric_value"],
"rows": [{ "dimension": "Anthropic", "metric_value": 128.4 }],
"view_spec": {}
}
```
`rows` is abbreviated above; a real chart carries every row it found.
Charts arrive decoded — `columns` and `rows`, not a chart-library option — and `view_spec` is the query that produced them, for reuse or refinement.
`chart_type` is one of `bar`, `line`, `area`, `stacked_bar`, `pie`, `donut`, `scatter`, `heatmap`, `kpi` or `table`.
A chart block carries `decode_failed: true` only when its data could not be read, so empty `rows` from a broken payload are distinguishable from a genuinely empty answer.
## Exit codes
| Code | Meaning |
| ---- | ---------------------------------------------------------------------------------------------------------------- |
| `0` | Success, including a turn that ends by asking you a question |
| `1` | No blocks returned, auth failure, role error, network failure, a message over 4,000 characters, or a write error |
# Discovery
Source: https://docs.getunbound.ai/cli/discovery
Scan devices for installed AI tools and schedule recurring scans
`unbound discover` scans a device for installed AI coding tools and reports findings to Unbound. This powers the AI Tools inventory and fleet visibility in the dashboard.
Tools detected: Cursor, Claude Code, Gemini CLI, Codex, Windsurf, Roo Code, Cline, GitHub Copilot, Augment Code, JetBrains, and more.
## Prerequisites
Python 3 must be available on the device — the installer accepts `python3`, `python`, or `py -3` on Windows. It downloads its files with Git when Git is available; the fallback needs `curl` and `tar` on macOS and Linux, and nothing extra on Windows.
Which key you pass to `--api-key` depends on the scan's scope:
* **Single-user scan** (no `sudo`) — use your own user API key.
* **All-users scan** (with `sudo`) — use your **admin API key**. The device is attributed to its owner automatically (resolved from the hardware serial); no separate discovery key is needed.
## Run a scan
```bash theme={null}
# Scan the current user only:
unbound discover --api-key
# Scan all users on the device (requires root):
sudo unbound discover --api-key
# Scan against a custom backend:
sudo unbound discover --api-key --domain https://backend.acme.com
```
## Schedule recurring scans
Keep the device's inventory current without re-running scans by hand:
```bash theme={null}
unbound discover --set-cron --api-key
```
The scan runs immediately, then once a day at 09:00 local time. Scheduling works on macOS, Windows, and Linux, and the schedule persists across reboots.
Check schedule status and log paths:
```bash theme={null}
unbound discover status
```
`discover status` reads launchctl state, so it only works on macOS. On other platforms it exits with an error naming the equivalent command:
* **Linux** — `systemctl --user status unbound-scheduled.timer`
* **Windows** — `Get-ScheduledTask -TaskName "ai.getunbound.scheduled"`
Remove the schedule:
```bash theme={null}
unbound discover unschedule
```
***
## One-step user onboarding
`unbound onboard` combines tool setup and discovery into a single command — useful for scripting new user enrollment:
```bash theme={null}
unbound onboard --api-key
```
This installs the tool bundle for your scope and then runs a device scan. Without `sudo` the bundle is Cursor, Claude Code hooks, Codex hooks, Copilot hooks and Augment hooks; with `sudo` it additionally includes Gemini CLI.
### Onboard flags
| Flag | Purpose |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--api-key` | User or admin key — gates which scope `onboard` runs in (see MDM section below) |
| `--domain` | Where the discovery scan reports to. Defaults to the backend you set with `unbound config set-backend-url`, falling back to `https://backend.getunbound.ai` when that is unset. It does **not** change where your installed tools send traffic — a private-gateway tenant sets those separately |
| `--set-cron` | Schedule a recurring daily scan after onboarding finishes. Works in both scopes: without `sudo` the daily job re-runs `onboard`; with `sudo` it schedules a **discovery scan only**, so your MDM platform still owns tool-deployment cadence |
| `--backfill` | Seed historical Claude Code / Codex / Copilot sessions from the device's local transcripts into Unbound analytics. Cursor and Augment are skipped automatically |
***
## MDM device onboarding (admin, requires root)
For fleet-wide enrollment from an MDM system:
```bash theme={null}
sudo unbound onboard --api-key
```
Scope is automatic: running `unbound onboard` **with `sudo`** configures every user on the device (MDM/org scope), while **without `sudo`** it sets up just the current user. Either way the command finishes with a device scan: under `sudo` it scans every user on the device, without `sudo` just the current user.
See [MDM Integrations](/mdm-integrations/overview) for deployment guides and MDM-specific configuration.
***
## Supported AI coding tools
Unbound gives security teams visibility into the AI coding tools your developers use — the agents themselves, the MCP servers they connect to, the rules files that steer them, and the skills they've installed. The matrix below summarizes coverage across the most common tools in the ecosystem today.
Coverage continues to evolve alongside the AI tooling ecosystem. Specific feature behavior may vary by tool version and deployment configuration. Discovery supports macOS, Windows, and Linux.
| Tool | Platform | Agents | MCP Servers | Rules | Skills |
| ---------------------------------------------------------------------------------------------------------------------------------- | --------------------- | :-------------------: | :-------------------: | :-------------------: | :-------------------: |
| **Cursor** | Windows, macOS, Linux | | | | |
| **Cursor CLI** | Windows, macOS, Linux | | | | — |
| **Claude Code CLI** | Windows, macOS, Linux | | | | |
| **Claude Cowork** | Windows, macOS, Linux | | — | — | |
| **GitHub Copilot** | Windows, macOS, Linux | | | | |
| **GitHub Copilot CLI** | Windows, macOS, Linux | | | | |
| **Augment Code** | Windows, macOS, Linux | | | | |
| **Gemini CLI** | Windows, macOS, Linux | | | | |
| **Codex** | Windows, macOS, Linux | | | | |
| **JetBrains IDEs** *(IntelliJ IDEA, PyCharm, WebStorm, PhpStorm, GoLand, Rider, CLion, RubyMine, DataGrip, RustRover, and others)* | Windows, macOS, Linux | | | | — |
| **Junie** *(JetBrains)* | Windows, macOS, Linux | | | | |
| **Roo Code** | Windows, macOS, Linux | | | | |
| **Cline** | Windows, macOS, Linux | | | | |
| **Kilo Code** | Windows, macOS, Linux | | | | |
| **OpenCode** | Windows, macOS, Linux | | | | |
| **OpenClaw** | Windows, macOS, Linux | | — | — | — |
| **Replit** | Windows, macOS, Linux | | — | — | |
| **Windsurf** | Windows, macOS, Linux | | | | |
| **Google Antigravity** | Windows, macOS, Linux | | | | — |
**Agents** — Detect installations of the AI coding tool.
**MCP Servers** — Surface the MCP server connections each tool has configured.
**Rules** — Capture the custom instruction and rules files each tool loads (e.g. `.cursorrules`, `CLAUDE.md`, `GEMINI.md`, `.windsurfrules`).
**Skills** — A `SKILL.md` file of instructions that an agent adds to its toolkit and uses when relevant to extend what it can do.
Connect your MDM provider and deploy AI tools at scale
View discovered tools and fleet activity in the dashboard
# Doctor
Source: https://docs.getunbound.ai/cli/doctor
Deep local health check for your Unbound install and connected AI tools
`unbound doctor` runs a deep diagnostic of your local Unbound install. It inspects every supported AI coding tool — Cursor, Claude Code, Codex, GitHub Copilot, and Augment Code — and validates that your stored API key is still accepted by the backend.
```bash theme={null}
unbound doctor
```
## What it checks
For **each AI coding tool**, doctor verifies three things:
| Check | What it confirms |
| --------------- | ------------------------------------------------------------------------------ |
| **Config** | The tool's own config file contains the Unbound integration |
| **Hook script** | The installed hook / key-helper file exists on disk |
| **Env wiring** | The API key — and the gateway URL for gateway mode — is exported in your shell |
It also validates your stored **API key** against the backend, so a revoked or expired key is caught here rather than at your next tool call.
For **Claude Code** and **Codex**, doctor reports the active **mode** (subscription vs gateway).
## Per-tool status
Each tool resolves to one of four statuses:
| Status | Meaning |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Healthy** | Config, hook script, and env wiring all present (shown in green) |
| **Tampered** | Installed but broken — a missing hook, a config without the integration, or a managed config that points at a hook that isn't on disk. Reinstall to fix |
| **Not set up** | No Unbound integration found for this tool |
| **Managed by MDM** | Configured org-wide by an admin (config **and** hook verified) |
Doctor always tells you the next step:
* **Nothing set up yet** → run `unbound setup`.
* **A user-level tool is tampered** → run `unbound doctor --fix` (no sudo needed).
* **An MDM-managed install is broken** → it's org-wide, so run `sudo unbound doctor --fix` to reinstall the managed hooks.
* **API key invalid** → run `unbound login`.
## Exit codes
| Code | Meaning |
| -------- | --------------------------------------------------------------------------- |
| `0` | All tools healthy (or not installed / MDM-managed) and the API key is valid |
| non-zero | Any tool is **Tampered** or the API key is invalid |
The non-zero exit lets you gate scripts or CI on a clean install.
## Flags
| Flag | Behavior |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--fix` | Reinstall the tampered tools the current scope can repair. **Without `sudo`** it fixes your user-level tools; **with `sudo`** (`sudo unbound doctor --fix`) it reinstalls the MDM-managed (org-wide) hooks. Whichever scope you run, doctor points you at the other for anything it couldn't touch |
| `--json` | Machine-readable output (per-tool `status`, `checks`, `conflict`, plus `api_key`) |
The JSON mirrors the human report. Each tool carries its `status`, the individual `checks`, and a boolean **`conflict`** — `true` when two modes of the same tool are installed at once (for example Claude Code in **both** subscription and gateway mode), which doctor reports as **Tampered** so `--fix` can clean it up. `api_key` carries the key-validation result (`valid` / `invalid` / `not-logged-in` / `unverified`).
## Examples
```bash theme={null}
# Full health check
unbound doctor
# Repair every tampered tool automatically
unbound doctor --fix
# Machine-readable output for scripts / CI
unbound doctor --json
```
# Exports
Source: https://docs.getunbound.ai/cli/exports
Export Logs, Tasks, Users and Tool Use data as CSV from the terminal
`unbound download` exports your organization's data as CSV — the same exports offered in the dashboard, from the terminal.
Requires **Admin or Analytics Viewer** role. Run `unbound status` to check.
## What can be exported
```bash theme={null}
unbound download kinds
```
| Kind | What it holds |
| ------------------------ | ------------------------------------------------------------------------------------- |
| `logs-export` | Every LLM call: prompt, model, tokens, status, user and task ID |
| `logs-tasks-export` | One row per task (agent session), with its thread ID |
| `analytics-export` | Tool Use rows — needs `dataset=terminal-runs\|mcp-actions\|unsanctioned\|mcp-servers` |
| `analytics-tasks-export` | Analytics → Tasks, optionally narrowed to one heatmap day |
| `users-export` | Analytics → Users: per-user cost, tokens and activity |
`unbound download kinds` also lists the filters each one accepts. It reads a list the CLI carries rather than asking the server, so a kind added to Unbound works before it appears here.
## Export something
```bash theme={null}
unbound download start logs-export --last 7d
unbound download start logs-export --last 24h --filter tool_types=CLAUDE_CODE -o today.csv
unbound download start logs-tasks-export --start 2026-07-01 --end 2026-07-31
unbound download start users-export --last 30d
unbound download start analytics-export --filter dataset=terminal-runs --last 7d
```
The command starts the export, waits for it, and saves the file. Without `-o` it writes a file named by the server in the current directory, and refuses to overwrite one that already exists.
## Time range
| Flag | Range |
| ------------------ | ------------------------------------------------------------------------------- |
| `--last d\|h` | Relative, e.g. `--last 7d`, `--last 24h` |
| `--start`, `--end` | Absolute as `YYYY-MM-DD`, at most 90 days apart and no earlier than 90 days ago |
Omit both and the server applies its own default window.
## Filters
`--search ` matches free text, on the logs kinds only.
Repeat `--filter` with the same key to pass a list. Each value is sent separately, which is how the server reads list filters:
```bash theme={null}
unbound download start logs-export --last 7d \
--filter tool_types=CLAUDE_CODE --filter tool_types=CURSOR
```
An unknown filter **key** is silently ignored, so a typo widens the export rather than failing — `tool_type=CURSOR` returns every tool. An invalid **value** for a known key is rejected, with the accepted values listed. Run `--check` first and confirm the count moved.
## Check before you export
`--check` counts the rows without starting anything:
```bash theme={null}
unbound download start logs-export --check --last 7d
unbound download start logs-export --check --last 90d --json
```
```json theme={null}
{"row_count":18204,"limit":25000,"exceeds_limit":false,"exact":true}
```
An export of more than **25,000 rows** is refused up front, with the real count, so narrow the range or add filters rather than paging.
## If you stop waiting
A large export takes a few minutes. The job ID is printed when it starts:
```
ℹ Export 4f2a1c9e-1b2c-4d5e-8f90-a1b2c3d4e5f6 started.
⠸ Preparing export… 18,204 rows
```
Interrupting the wait does not cancel the export. Fetch the finished file within **6 hours**:
```bash theme={null}
unbound download get 4f2a1c9e-1b2c-4d5e-8f90-a1b2c3d4e5f6 -o logs.csv
```
`--no-wait` starts an export and returns the ID immediately, for a script that would rather collect it later:
```bash theme={null}
id=$(unbound download start logs-export --last 7d --no-wait --json | jq -r .id)
unbound download get "$id" -o logs.csv
```
Only **3 exports** may be preparing at once per organization. A fourth returns an error asking you to wait — do not poll by re-running the export.
## Output flags
| Flag | Behavior |
| --------------------- | ---------------------------------------------------------------- |
| `-o, --output ` | Write to this file instead of the server-chosen name |
| `--check` | Print how many rows would be exported, then exit |
| `--json` | Machine-readable output — use with `--check`, or to get a job ID |
| `--no-wait` | Start the export and print its job ID instead of waiting |
## For scripts and agents
An export is prepared by a background worker, so it is three steps: start it, poll until it is ready, then download it. `unbound download start` does all three and blocks until the file is on disk.
Without `-o`, the destination depends on where output is going: a file named by the server when you are at a terminal, and stdout when it is piped or redirected. Diagnostics go to stderr either way, so the stream stays clean. The exit code is `0` on success and `1` on any error.
```bash theme={null}
unbound download start logs-export --last 24h | head -5
```
Requires CLI **1.15.0** or later. Tool Use data can also be exported with `unbound analytics tool-use --format csv`, which uses this same flow — see [Analytics](/cli/analytics).
# Unbound CLI
Source: https://docs.getunbound.ai/cli/overview
Install and manage the Unbound AI Gateway from your terminal
The Unbound CLI (`unbound`) lets you authenticate, configure AI coding tools, manage policies, inspect usage, and administer your organization — all without leaving the terminal.
## Installation
Requires **Node.js 18+**.
```bash theme={null}
npm install -g unbound-cli
```
On Linux installs where Node lives in a system path (e.g. the NodeSource `apt` package), `npm install -g` writes to a root-owned directory and needs `sudo`:
```bash theme={null}
sudo npm install -g unbound-cli
```
With a user-scoped Node manager (`nvm`, `fnm`, `volta`) or a custom `npm prefix`, no `sudo` is required.
Verify:
```bash theme={null}
unbound --version
```
Both `unbound` and `unbound-cli` are installed and point to the same binary. If `unbound` resolves to a different program, see [CLI Path Conflicts](/support/troubleshooting/cli-path-errors).
Credentials and URL settings are stored at `~/.unbound/config.json`, created automatically on first login.
### Environment overrides
Three environment variables take precedence over anything stored in the config file:
| Variable | Overrides |
| ---------------------- | ------------------------------------------------------- |
| `UNBOUND_API_URL` | The **backend** URL — despite the name, not the gateway |
| `UNBOUND_FRONTEND_URL` | The frontend URL |
| `UNBOUND_GATEWAY_URL` | The gateway URL |
If a command reaches the wrong host after you ran `unbound config urls`, check these first — a stale export from an earlier shell silently shadows the file. `unbound config show` prints the values actually in effect.
## Commands at a glance
| Command | What it does |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `unbound login` | Authenticate via browser or API key |
| `unbound setup ` | Configure an AI coding tool to route through Unbound |
| `unbound onboard` | Set up all tools and run a device scan in one step — useful for scripting new user enrollment |
| `unbound nuke` | Remove Unbound entirely — tools and credentials (run with `sudo` to also clear device-wide MDM config) |
| `unbound policy ` | Create, list, and manage policies |
| `unbound users list` | List organization members |
| `unbound user-groups list` | List and manage user groups |
| `unbound tools list` | See connected AI tools |
| `unbound discover` | Scan this device for installed AI tools |
| `unbound chat` | Query usage data in natural language |
| `unbound analytics tool-use ` | Read Tool Use data — terminal commands and MCP tool calls |
| `unbound download start ` | Export Logs, Tasks, Users or Tool Use data as CSV |
| `unbound status` | Check CLI config and API connectivity, your role, and connected tools |
| `unbound doctor` | Deep per-tool health check of your local install |
| `unbound config ` | View or change the gateway / frontend / backend URLs without re-logging in (see [Configuration](#configuration) below) |
Use `unbound --help` for flag details on any command.
## Configuration
`unbound config` reads and writes the three URLs stored in `~/.unbound/config.json` (gateway, frontend, backend) without forcing you to re-run `unbound login`. Useful for tenant / custom-domain installs, switching between staging and production, or recovering from a bad URL set.
| Subcommand | What it does |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `unbound config show` | Print the current gateway, frontend, and backend URLs (plus login state). Add `--json` for machine-readable output. |
| `unbound config urls ` | Set all three URLs in one call. Positional order matters: gateway → frontend → backend. |
| `unbound config set-gateway-url ` | Update only the gateway URL (LLM proxy — what the SDK and tool hooks talk to). |
| `unbound config set-frontend-url ` | Update only the frontend URL (the dashboard / workspace UI). |
| `unbound config set-backend-url ` | Update only the backend URL (CLI API — auth, policy, usage queries). |
Example — point the CLI at a tenant install without logging out. Positional order is **gateway → frontend → backend**. Watch the naming: the first argument is the *gateway* (LLM proxy) URL, even though the dashboard host is conventionally `gateway.`.
```bash theme={null}
# Positional order:
unbound config urls https://api.acme.com https://gateway.acme.com https://backend.acme.com
unbound config show
```
See [Tenant / Custom Domains](/cli/authentication#tenant-custom-domains) for the full custom-domain setup, including passing the same URLs at login time.
Login, logout, status, and tenant URL config
Configure AI tools to route through Unbound
Create and manage all four policy types
Manage members, user groups, and tool connections
Scan devices for AI tools and schedule recurring scans
Query usage data with natural language
# Policy Management
Source: https://docs.getunbound.ai/cli/policies
Create, list, and manage all four Unbound policy types from the CLI
Unbound has three policy types: **Cost**, **Security**, and **Tool**. Each has its own subcommand for type-specific creation. Generic commands (list, get, delete) work across Cost and Security; Tool policies use a separate `policy tool` subcommand.
## Reference data
Before creating policies, pull the available user groups, models, guardrails, and tool types:
```bash theme={null}
unbound policy form-data
```
## Generic commands (Cost / Model / Security)
```bash theme={null}
unbound policy list # List all policies
unbound policy list --type COST # Filter by type: COST, SECURITY
unbound policy list --enabled # Only show currently enabled policies
unbound policy list --search "budget" # Filter by name substring
unbound policy get # View a policy's full config
unbound policy delete # Delete a policy (prompts to confirm)
unbound policy delete --yes # Delete without the confirmation prompt
unbound policy effective # Effective policies for a user (default)
unbound policy effective --user # Same, stated explicitly
unbound policy effective --group # Treat as a user-group ID instead
```
Use the type-specific commands — `policy cost create`, `policy security create`, and the `policy tool` family below — for guided, flag-based create and update. Each also has an `update ` that changes only the fields you pass.
The same type filter applies to type-specific list and update commands:
```bash theme={null}
unbound policy cost list
unbound policy security list
unbound policy cost update --monthly-budget 2000
```
***
## Cost policies
Set a monthly spend limit per user group:
```bash theme={null}
unbound policy cost create \
--name "Engineering Budget" \
--monthly-budget 1000 \
--group engg
```
Scope it to a team, order it against your other policies, or stage it before it takes effect:
| Flag | Description |
| ---------------------- | ------------------------------------------------------------------------------------------ |
| `--group ` | Comma-separated user groups to scope the policy to. Omit to apply org-wide |
| `--priority ` | Lower numbers take precedence. Auto-assigned if omitted |
| `--disabled` | Create the policy without turning it on — useful for staging a change before you enable it |
```bash theme={null}
unbound policy cost create \
--name "Platform team budget" \
--monthly-budget 5000 \
--group platform \
--priority 10 \
--disabled
```
The same three flags work on `policy security create`.
See [Cost Policies](/policies/cost-policies) for all configuration options.
***
***
## Security policies
Apply guardrails for PII and secrets detection:
```bash theme={null}
# Block requests containing PII:
unbound policy security create \
--name "Block PII" \
--sub-type guardrails \
--guardrail PII:BLOCK
# Audit requests containing secrets:
unbound policy security create \
--name "Audit Secrets" \
--sub-type guardrails \
--guardrail SECRETS:AUDIT
```
See [Security Policies](/policies/security-policies) for all configuration options.
***
## Tool policies
Tool policies control shell commands and MCP tool calls. They use a separate backend and are reached via `unbound policy tool`.
### List and inspect
```bash theme={null}
unbound policy tool list
unbound policy tool list --type TERMINAL # or MCP
unbound policy tool list --search "rm" # match name, description, or command family
unbound policy tool get
unbound policy tool delete # prompts to confirm; add --yes to skip
```
Tool policies are paginated at 50 per page:
| Flag | Description |
| ----------------- | --------------------------------------------- |
| `--page ` | Page number (default 1) |
| `--page-size ` | Results per page (default 50) |
| `--all` | Fetch every page (may be slow for large orgs) |
### Discover available targets
```bash theme={null}
unbound policy tool families # Terminal command families and the fields they accept
unbound policy tool mcp-servers # Known MCP servers and their available tools
```
### Create a terminal command policy
```bash theme={null}
unbound policy tool create-terminal --no-ai \
--name "Block rm -rf" \
--command-family filesystem \
--field command='rm -rf*' \
--action BLOCK \
--custom-message "Destructive command blocked by policy."
```
#### Describe it in plain English
If you don't know the family or the field to match on, describe what you want and let Unbound author the policy:
```bash theme={null}
unbound policy tool create-terminal --prompt "block rm -rf"
```
You get a preview of the policy it drafted and a confirmation step before anything is created. Each `--prompt` run creates **exactly one policy**, so describe one rule at a time. Add `--yes` to skip the confirmation (the preview still prints), or `--no-ai` to use the raw classification flags instead.
`--prompt` can't be combined with `--command-family`, `--field`, or `--config`. You can still pass `--name`, `--description`, `--action`, `--custom-message`, `--group`, `--disabled`, `--yes` and `--json`.
`create-terminal` and `create-mcp` need you to pick a mode: either `--prompt ""` for AI assist, or `--no-ai` to pass the raw classification flags. Running either with neither exits `2`.
`--no-ai` is refused when the CLI runs inside Claude Code, so an agent can't bypass AI assist. Set `UNBOUND_ALLOW_NO_AI_UNDER_CLAUDE=1` if you're driving the terminal yourself.
Whenever the resolved action is `BLOCK` or `WARN`, `--custom-message` is required — those policies always need a user-facing message.
Tool policies can't scope by environment, project, time window, or exception clause. If your prompt mentions one — "production", "after hours", "except…" — the CLI warns that those parts will be ignored and asks whether to continue. Prompts are capped at 1,800 characters.
#### Match a command we don't classify
Most commands fall into a family (`filesystem`, `cloud_destroy`, `git_action`, …). When yours doesn't, or you want to match one exact command rather than a category, use the **`custom`** family and give it the whole command pattern:
```bash theme={null}
unbound policy tool create-terminal --no-ai \
--name "Block the release script" \
--command-family custom \
--field command='make release-prod' \
--action BLOCK \
--custom-message "Use the release pipeline instead."
```
`custom` takes a single `command` field — the pattern is matched against the whole command line. Run `unbound policy tool families` to see every family and the fields each one accepts.
### Create an MCP tool policy
```bash theme={null}
# Match by action type (e.g. all write operations on a server):
unbound policy tool create-mcp --no-ai \
--name "Audit Linear writes" \
--mcp-server Linear \
--mcp-action-type write \
--action AUDIT
# Match a specific tool:
unbound policy tool create-mcp --no-ai \
--name "Block PR creation" \
--mcp-server GitHub \
--mcp-tool create_pull_request \
--action BLOCK \
--custom-message "Open pull requests through the normal review flow."
```
Available actions: `BLOCK`, `WARN`, `AUDIT`, `REQUIRE_SLACK_APPROVAL`
### Update a tool policy
Only the fields you pass are changed:
```bash theme={null}
unbound policy tool update --action WARN
```
| Flag | Applies to | Description |
| --------------------------- | ---------- | ------------------------------------------------------------------- |
| `--name ` | both | New policy name |
| `--description ` | both | New description |
| `--action ` | both | `BLOCK`, `WARN`, `AUDIT`, or `REQUIRE_SLACK_APPROVAL` |
| `--custom-message ` | both | Message shown when the policy fires (required for `BLOCK` / `WARN`) |
| `--group ` | both | Replace the user-group scope |
| `--enabled` / `--disabled` | both | Turn the policy on or off |
| `--command-family ` | terminal | New command family |
| `--field ` | terminal | Replace match fields; repeatable (ANDed) |
| `--mcp-server ` | mcp | New MCP server |
| `--mcp-tool ` | mcp | New MCP tool (mutually exclusive with `--mcp-action-type`) |
| `--mcp-action-type ` | mcp | New action type: `read`, `write`, `destructive` |
The cost and security policy types each have an `update ` too. `policy cost update` takes `--monthly-budget` plus `--name`, `--group`, `--priority` and `--enabled` / `--disabled`. `policy security update` takes those shared flags, but guardrail and routing changes go through `--config ''` — the create-time `--sub-type` / `--guardrail` / `--route` flags aren't accepted on update:
```bash theme={null}
unbound policy cost update --monthly-budget 2000
unbound policy security update --disabled
```
***
See [Tool Policies](/policies/tool-policies) for the full list of actions, NL rules, and canonical group targeting.
Every list, get, create and update command supports `--json` for machine-readable output. `delete` doesn't — it takes only `--yes`.
Dashboard reference for actions, NL rules, and Slack approvals
Dashboard reference for budget limits and spend controls
# Command Reference
Source: https://docs.getunbound.ai/cli/reference
Every unbound command and flag, with defaults, constraints and exit codes
Every command the CLI accepts, with exact flag spellings. The task-oriented pages explain *when* to use these; this page is the lookup.
All long flags take **two** dashes. `-o`, `-y`, `-m` and `-c` are the only short flags in the CLI.
## Global
```bash theme={null}
unbound [--version] [--help] [subcommand] [options]
```
Binaries are `unbound` and `unbound-cli` — the same program. Requires Node 18 or later.
## Account
| Command | Flags |
| ---------------- | ---------------------------------------------------------------------------------------------------------------- |
| `unbound login` | `--api-key ` · `--gateway-url ` · `--frontend-url ` · `--backend-url ` · `--domain ` |
| `unbound logout` | none — clears credentials, keeps your URL settings |
| `unbound status` | `--json` |
Omit `--api-key` on login and a browser opens instead. That flow times out after 120 seconds, so pass `--api-key` on a headless machine.
## Health
| Command | Flags |
| ---------------- | ------------------ |
| `unbound doctor` | `--fix` · `--json` |
`doctor` exits non-zero when a tool is tampered, your API key is invalid, **or** `CLAUDE_CONFIG_DIR` is set-but-empty or points somewhere other than your install. The last two fail the run **with every tool healthy** — see [Doctor](/cli/doctor).
## Setup
| Command | Flags |
| ------------------------------------ | --------------------------------------------------------------------------------------- |
| `unbound setup [tools...]` | `--api-key ` · `--clear` · `--subscription` · `--gateway` · `--all` · `--backfill` |
| `unbound nuke` *(alias `uninstall`)* | `-y, --yes` |
| `unbound onboard` | `--api-key ` · `--domain ` · `--set-cron` · `--backfill` |
| `unbound onboard unschedule` | none |
**Tool names.** `cursor`, `copilot`, `augment`, `claude-code`, `claude-code-subscription`, `claude-code-gateway`, `gemini-cli`, `codex`, `codex-subscription`, `codex-gateway`, `roo-code`, `cline`, `kilo-code`, `custom-access`.
**Constraints.** `--subscription` and `--gateway` are mutually exclusive and apply only to Claude Code and Codex. `--all` cannot be combined with explicit tool names. Scope follows privileges — there is no scope flag; run under `sudo` for org-wide.
`unbound setup` runs a per-tool installer written in Python, so **Python 3 must be on your `PATH`**. Without it, setup exits with `Python 3 not found`. Instruction-only tools — `roo-code`, `cline`, `kilo-code`, `custom-access` — don't need it.
## Policies
| Command | Flags |
| ------------------------------- | ----------------------------------------------------------------------------- |
| `unbound policy list` | `--type ` · `--enabled` · `--search ` · `--json` |
| `unbound policy get ` | `--json` |
| `unbound policy delete ` | `--yes` — **no `--json`** |
| `unbound policy form-data` | `--json` |
| `unbound policy effective ` | `--user` · `--group` · `--json` |
`--user` and `--group` are **booleans**; `` is a required positional. `--group` treats the id as a user-group id, `--user` is the default.
There is also a raw-config pair, `unbound policy create` and `unbound policy update `, taking `--name`, `--type ` and `--config ` (all three required on create) plus `--scope-groups `, `--scope-tools `, `--priority ` and `--enabled` / `--no-enabled`. Prefer the type-specific commands below — the CLI's own help says so.
### Cost and security
| Command | Flags |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `policy cost create` | `--name` *(required)* · `--monthly-budget ` · `--group` · `--priority` · `--disabled` · `--config` · `--json` |
| `policy cost update ` | as create, plus `--enabled` / `--disabled` |
| `policy security create` | `--name` *(required)* · `--sub-type ` · `--guardrail ` · `--route-model` · `--route ` · `--error-route ` · `--group` · `--priority` · `--disabled` · `--config` · `--json` |
| `policy security update ` | `--name` · `--group` · `--priority` · `--enabled` / `--disabled` · `--config` · `--json` |
`--guardrail`, `--route` and `--error-route` are repeatable. **`policy security update` does not accept the create-time type flags** — guardrail and routing changes go through `--config ''`.
### Tool policies
| Command | Flags |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `policy tool list` | `--type ` · `--search` · `--page` · `--page-size` · `--all` · `--json` |
| `policy tool get ` | `--json` |
| `policy tool delete ` | `--yes` |
| `policy tool create-terminal` | `--prompt ` **XOR** `--no-ai` · `--name` · `--command-family` · `--field ` · `--action` · `--description` · `--custom-message` · `--group` · `--disabled` · `--config` · `--yes` · `--json` |
| `policy tool create-mcp` | `--prompt` **XOR** `--no-ai` · `--name` · `--mcp-server` · `--mcp-tool` **XOR** `--mcp-action-type` · `--action` · plus the same shared flags |
| `policy tool update ` | `--name` · `--description` · `--action` · `--custom-message` · `--group` · `--enabled` / `--disabled` · type-specific match flags · `--config` · `--json` |
| `policy tool families` | `--json` |
| `policy tool mcp-servers` | `--json` |
**Both create commands require a mode.** Pass `--prompt` for AI assist or `--no-ai` for raw flags; with neither the command exits `2`. `--field` is repeatable and ANDed. `--action` is one of `AUDIT`, `BLOCK`, `WARN`, `REQUIRE_SLACK_APPROVAL`, and **`BLOCK` or `WARN` always requires `--custom-message`**.
## Users and groups
| Command | Flags |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `unbound users list` | `--json` |
| `unbound users effective-policies ` | `--json` |
| `unbound user-groups list` *(alias `groups`)* | `--search ` · `--json` |
| `unbound user-groups get ` | `--json` |
| `unbound user-groups create` | `--name` *(required)* · `--all-org-users` / `--no-all-org-users` · `--user-ids ` — **no `--json`** |
| `unbound user-groups update ` | same set — **no `--json`** |
| `unbound user-groups delete ` | `--yes` — **no `--json`** |
| `unbound user-groups effective-policies ` | `--json` |
| `unbound tools list` | `--json` |
| `unbound tools connect ` | `--json` |
| `unbound tools approved` | `--json` |
`--user-ids` takes **numeric** ids, as shown in the `ID` column of `unbound users list`. A non-numeric value is silently dropped rather than rejected, so the member simply doesn't get added.
`unbound tools connect` prints the newly generated API key in plaintext. Redact it before pasting that output anywhere.
## Analytics and exports
| Command | Flags |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `unbound analytics tool-use ` | see below |
| `unbound download kinds` | `--json` |
| `unbound download start ` | `--last ` · `--start` · `--end` · `--search` · `--filter ` · `-o ` · `--check` · `--json` · `--no-wait` |
| `unbound download get ` | `-o ` |
Datasets: `terminal`, `mcp-actions`, `unsanctioned`, `mcp-servers`.
Shared flags: `--time-window` · `--start` / `--end` · `--tool` · `--user` · `--risk` · `--policy` · `--format ` · `--json` · `-o` · `--limit` · `--offset` · `--wide`.
Per-dataset flags — this is the part that surprises people:
| Dataset | `--family` | `--server` | `--initiator` |
| -------------- | :--------: | :--------: | :-----------: |
| `terminal` | ✅ | — | ✅ |
| `mcp-actions` | — | ✅ | ✅ |
| `unsanctioned` | — | ✅ | — |
| `mcp-servers` | — | ✅ | — |
Passing one where it isn't accepted fails with `unknown option`.
**Limits.** `--limit` defaults to 20, caps at 100 for a table and 1,000 for JSON, and **over the cap the command exits rather than clamping**. `-o` requires `--format json` or `--format csv`. Custom date ranges must be used in pairs and span 90 days or less. Exports cap at 25,000 rows and are kept for 6 hours.
Both need **Admin or Analytics Viewer**.
## Chat
| Command | Flags |
| -------------- | ---------------------------------------------------------------------------------------------- |
| `unbound chat` | `-m, --message ` · `--json` · `-o, --output ` · `-c, --continue ` |
No flags starts an interactive session. `--json`, `-o` and `-c` each require a non-empty `-m`. `-c` takes the `conversation_id` from a previous reply. Messages cap at 4,000 characters. Needs **Admin, Analytics Viewer or Manager**.
## Discovery
| Command | Flags |
| ----------------------------- | ---------------------------------------------------------------- |
| `unbound discover` | `--api-key ` *(required)* · `--domain ` · `--set-cron` |
| `unbound discover unschedule` | none |
| `unbound discover status` | none — **macOS only** |
On Linux use `systemctl --user status unbound-scheduled.timer`; on Windows `Get-ScheduledTask -TaskName "ai.getunbound.scheduled"`.
## Configuration
| Command | Flags |
| ---------------------------------------------------- | --------------------------------------- |
| `unbound config urls ` | three required positionals, fixed order |
| `unbound config set-backend-url ` | none |
| `unbound config set-frontend-url ` | none |
| `unbound config set-gateway-url ` | none |
| `unbound config show` | `--json` |
Settings live at `~/.unbound/config.json`.
### Environment variables
| Variable | Effect |
| ------------------------- | ----------------------------------------------------------------------------------- |
| `UNBOUND_API_URL` | Sets the **backend** URL — despite the name, not the gateway |
| `UNBOUND_FRONTEND_URL` | Sets the frontend URL |
| `UNBOUND_GATEWAY_URL` | Sets the gateway URL |
| `UNBOUND_API_KEY` | Fallback API key for `onboard` |
| `CLAUDE_CONFIG_DIR` | Where Claude Code hooks are installed and verified. Set-but-empty is a hard failure |
| `UNBOUND_NO_UPDATE_CHECK` | Any value disables the update check |
| `NO_COLOR` | Disables coloured output |
The three URL variables **override anything in the config file**, silently. If a command reaches the wrong host after you ran `unbound config urls`, check these first — a stale export from an earlier shell shadows the file. `unbound config show` prints what's actually in effect.
## Exit codes
| Code | Meaning |
| ----- | -------------------------------------------------------------------------- |
| `0` | Success, or a confirmation you declined |
| `1` | General failure — invalid input, network, or a failed operation |
| `2` | Validation or steering error, such as a missing required mode flag |
| `3` | Authentication or role failure — not logged in, or the command needs Admin |
| `130` | Interrupted with Ctrl-C |
The direct installers `unbound setup` calls also use `3`, but there it means the device is MDM-managed and the install was skipped. `unbound setup` reports that as a skip rather than passing the code through.
# Tool Setup
Source: https://docs.getunbound.ai/cli/tool-setup
Configure AI coding tools to route through Unbound using the CLI
The `unbound setup` command configures AI coding tools to route through Unbound. It handles authentication, environment variables, and tool-specific config files automatically.
If you are not logged in and `--api-key` is not provided, the browser opens automatically to authenticate before setup begins.
## Interactive setup
Run with no arguments to select tools from an interactive menu:
```bash theme={null}
unbound setup
```
## Default bundle
Install Cursor, Claude Code (hooks), Codex (hooks), Copilot (hooks), and Augment (hooks) in one command:
```bash theme={null}
unbound setup --all
```
## Individual tools
### Cursor
```bash theme={null}
unbound setup cursor
```
### Claude Code
Claude Code supports two modes:
```bash theme={null}
unbound setup claude-code --gateway # Unbound as the AI provider — no Anthropic subscription needed
unbound setup claude-code --subscription # Hooks only — keep your existing Anthropic subscription
```
Run without a flag for interactive mode selection.
The bare `unbound setup claude-code` (no flag) and `unbound setup codex` (no flag) commands open an interactive prompt to pick a mode. In a non-interactive shell (CI, SSH with `< /dev/null`, MDM bootstrap) the prompt has no input source and the command exits immediately without writing any config. Always pass `--gateway` or `--subscription` explicitly in headless environments.
### Gemini CLI
```bash theme={null}
unbound setup gemini-cli
```
### Codex
```bash theme={null}
unbound setup codex --gateway # Unbound as the AI provider — no OpenAI subscription needed
unbound setup codex --subscription # Hooks only — keep your existing OpenAI subscription
```
### GitHub Copilot
```bash theme={null}
unbound setup copilot
```
Hooks-based — works with both the VS Code extension and the `copilot` CLI on your existing Copilot subscription.
### Augment Code
```bash theme={null}
unbound setup augment
```
Hooks-based — works with both the Augment VS Code extension and the Auggie CLI.
## Instruction-only tools
These tools require manual configuration. The CLI prints the API key and base URL to enter in each tool's settings.
| Tool | Command |
| -------------------------- | ----------------------------- |
| Roo Code | `unbound setup roo-code` |
| Cline | `unbound setup cline` |
| Kilo Code | `unbound setup kilo-code` |
| Custom Access (direct API) | `unbound setup custom-access` |
## One-step login + setup
Combine authentication and setup in a single command with `--api-key`:
```bash theme={null}
# Single tool:
unbound setup cursor --api-key
# Multiple tools (use explicit mode names when combining):
unbound setup cursor claude-code-gateway --api-key
# Login, then select interactively:
unbound setup --api-key
```
When specifying multiple tools in one command, use the explicit form names:
| Short form | Explicit form |
| ---------------------------- | -------------------------- |
| `claude-code --gateway` | `claude-code-gateway` |
| `claude-code --subscription` | `claude-code-subscription` |
| `codex --gateway` | `codex-gateway` |
| `codex --subscription` | `codex-subscription` |
## Remove configuration
```bash theme={null}
unbound setup cursor --clear
unbound setup claude-code --clear
unbound setup gemini-cli --clear
unbound setup codex --clear
unbound setup copilot --clear
unbound setup augment --clear
```
Remove every tool's configuration in one command:
```bash theme={null}
unbound setup --all --clear
```
## Uninstall everything
`unbound nuke` (alias `unbound uninstall`) removes Unbound entirely and returns the CLI to a fresh state: it clears tool configuration and deletes your stored credentials (`~/.unbound/config.json`). When it finishes, run `unbound login` (or `unbound onboard`) to set things up again.
The scope follows your privileges — no flag to pick:
```bash theme={null}
# Without root: clears your tools + credentials (MDM/system-level config is skipped)
unbound nuke
# With root: also clears MDM (system-level) config for all users on the device
sudo unbound nuke
```
Add `--yes` to skip the confirmation prompt.
***
## Setup flags
| Flag | Purpose |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--api-key` | Authenticate inline rather than via the browser flow |
| `--subscription` | Claude Code / Codex in hooks mode — keep your own subscription |
| `--gateway` | Claude Code / Codex with Unbound as the AI provider |
| `--all` | Install the default bundle (Cursor + Claude Code hooks + Codex hooks + Copilot hooks + Augment hooks). Cannot be combined with explicit tool names |
| `--clear` | Remove a tool's configuration instead of installing it |
| `--backfill` | Seed historical Claude Code / Codex / Copilot sessions from local transcripts into Unbound analytics, so past activity shows up. Subscription/hooks mode only — not supported in gateway mode, and Cursor and Augment are unsupported |
***
## One-step onboarding
`unbound onboard` runs `setup --all` followed by device discovery in a single command. Useful for new user enrollment:
```bash theme={null}
unbound onboard --api-key
```
***
## MDM setup (admin, requires root)
Scope is automatic: run `unbound setup` / `unbound onboard` **with `sudo`** to configure every user on the device (MDM/org scope), or **without `sudo`** to set up just the current user. For organization admins enrolling devices via MDM, run `unbound setup` with `sudo` to configure all users on the device:
```bash theme={null}
# Install the default bundle for all users:
sudo unbound setup --api-key KEY --all
# Specific tools:
sudo unbound setup --api-key KEY cursor codex-subscription
sudo unbound setup --api-key KEY claude-code-subscription codex-subscription gemini-cli
# Remove config:
sudo unbound setup --clear cursor
```
For device-wide onboarding in one step:
```bash theme={null}
sudo unbound onboard --api-key
```
MDM setup requires root and is intended for administrators deploying Unbound across an organization's device fleet. See [MDM Integrations](/mdm-integrations/overview) for deployment guides.
# Users & Groups
Source: https://docs.getunbound.ai/cli/user-management
Manage organization members, user groups, and tool connections from the CLI
## Users
```bash theme={null}
unbound users list # List all organization members
unbound users effective-policies # View effective policies for a specific user
```
***
## User groups
Groups let you scope policies to specific teams or roles. Policy changes apply automatically to all group members.
```bash theme={null}
unbound user-groups list # List all groups (alias: unbound groups list)
unbound user-groups get # View a group and its members
unbound user-groups create --name "Security Team"
unbound user-groups update --name "New Name"
unbound user-groups delete # Prompts for confirmation (skip with --yes)
unbound user-groups effective-policies # Effective policies applied to a group
```
### Membership flags on create / update
`create` and `update` accept the same membership flags. You can seed a group from every user in your org, opt out of that default, or pass an explicit set of user IDs:
| Flag | Purpose |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--all-org-users` | Include every current member of the organization in the group |
| `--no-all-org-users` | Explicit opt-out; useful on `update` to clear a previously org-wide group |
| `--user-ids` | Comma-separated **numeric** user IDs, as shown in the `ID` column of `unbound users list`. Replaces the current membership on `update`. A non-numeric value is silently dropped rather than rejected |
```bash theme={null}
# Seed a "Reviewers" group from the whole org:
unbound user-groups create --name "Reviewers" --all-org-users
# Replace membership on update:
unbound user-groups update --user-ids 42,57,103
```
***
## Tool connections
View which AI tools are connected to Unbound across the organization, and manage approved tool types.
```bash theme={null}
unbound tools list # List all connected tools and their status
unbound tools connect # Connect a new tool (generates an API key)
unbound tools approved # List approved tool types and whether restrictions are enforced
```
`unbound tools connect` prints the newly generated API key in plaintext. Treat that output as a secret — redact it before pasting it into a ticket or a chat, and re-run `connect` to issue a fresh key if one is exposed.
Supported tool types: `CLAUDE_CODE`, `CURSOR`, `COPILOT`, `AUGMENT_CODE`, `ROO_CODE`, `CLINE`, `GEMINI_CLI`, `CODEX`, `UNBOUND_CODEX`, `KILO_CODE`, `CUSTOM_ACCESS`
***
`users list`, `users effective-policies`, `user-groups list`, `user-groups get`, `user-groups effective-policies`, `tools list`, `tools connect` and `tools approved` support `--json`. `user-groups create`, `update` and `delete` don't.
# Frequently Asked Questions
Source: https://docs.getunbound.ai/faqs
Common questions about Unbound Security
## Frequently Asked Questions
### How does Unbound handle sensitive information?
Unbound does not store any prompts that are not explicitly flagged for logging. Logged prompts can also be written to enterprise data stores in public cloud infrastructure with write only access roles. In all other cases, Unbound's analytics and decision making is restricted to in-memory processing.
### What is your detection rate for secrets?
Unbound has advanced secret detection capabilities that can identify and redact sensitive information before it reaches LLM providers. The system has been proven to catch hundreds of secrets that would otherwise be training fodder for LLMs, with high accuracy rates across various types of sensitive data.
### Can I bring in my fine-tuned model into Unbound to use with my AI tools?
Yes, Unbound supports bringing your own custom models. The Enterprise plan includes support for custom models, allowing you to integrate your fine-tuned models with your existing AI tools through the Unbound gateway.
### Can I host Unbound on my own?
Unbound offers flexible deployment options. You can integrate with your existing infrastructure and route requests through your own systems while maintaining the security and control benefits of the Unbound platform.
### How does Unbound integrate with existing AI tools?
Unbound integrates with 50+ enterprise AI tools and popular coding applications like Roo Code, Cursor, and Cline. It works as an HTTP proxy, so you simply configure your AI tools to use Unbound as the API provider instead of calling LLM APIs directly.
### What kind of cost savings can I expect?
Organizations typically save up to 80% on coding agent costs by using Unbound's intelligent routing. The system automatically routes requests to the most cost-effective models based on complexity and requirements, rather than using the most expensive models for every request.
### How does Unbound handle model downtime and rate limits?
Unbound provides automatic error handling and redundancy. When rate limit errors or downtimes occur (which can range from 74-96% uptime for top LLM providers), Unbound automatically routes requests to other configured models, ensuring your developers stay productive.
### What analytics and insights does Unbound provide?
Unbound offers organization-wide visibility including:
* Deep usage insights at user, team, and department levels
* Identification of power users and users who need coaching
* File-level analytics showing which files are most touched by AI
* Cost optimization recommendations
* Usage patterns and trends
### Is there a free trial available?
Yes, Unbound offers a free trial that includes access to core features and models. The free trial provides a great way to test the platform's capabilities before committing to an enterprise plan.
### How does Unbound ensure data privacy?
Unbound implements multiple layers of data protection:
* Automatic detection and redaction of sensitive information
* Routing sensitive requests to private models or confidential computing infrastructure
* In-memory processing for analytics (no persistent storage unless explicitly configured)
* Enterprise-grade security controls and compliance features
### What authentication methods are supported?
Unbound supports enterprise authentication methods including SAML, OIDC, and SCIM integration, making it easy to integrate with your existing identity management systems.
### Can I use Unbound with multiple LLM providers?
Yes, Unbound supports multiple LLM providers and allows you to bring your own API keys. You can configure routing rules to use different providers based on your specific requirements for cost, performance, and security.
#
Source: https://docs.getunbound.ai/index
# Control What AI Agents Can Do with Unbound
Discover, assess, and enforce policy for AI coding agents across your org. The Agent Access Security Broker (AASB) gives security teams full visibility and control over every agent, tool, and action.
**New to Unbound?** Start with our [Introduction](/introduction) to understand the platform, then [install the CLI](/cli/overview) to get started in minutes.
## Quick Start
Run `npm install -g unbound-cli` to install the Unbound command-line tool.
Run `unbound login` to authenticate via your browser.
Run `unbound setup cursor` (or `claude-code`, `gemini-cli`, `codex`, `copilot`, etc.) to configure your tool.
Monitor agent activity and enforce policy on every agent action from the Unbound dashboard.
## Get Started
Learn about Unbound Security, its capabilities, and how it governs AI coding agents in your organization
Install the CLI, login, and set up your AI tools in minutes
## Integrations
Command-line AI coding assistant with enterprise security
Google Gemini CLI
GitHub Copilot in VS Code and CLI
Augment Code in VS Code and the Auggie CLI
AI-powered coding assistant
All-in-one agentic engineering platform with IDE and CLI support
IDE extension for AI-powered coding
Command-line AI coding assistant with enterprise security
Cursor IDE integration
Import claude.ai chat activity for observability and audit trails
## Support & Resources
Find answers to common questions about Unbound Security
Contact our support team for technical assistance and questions
# Tool Policy Hooks (Inbound `/v1/hooks/pretool`)
Source: https://docs.getunbound.ai/integrations/api-tool-policy-hooks
Enforce tool policies on agent tool calls before execution. Inbound endpoint — your agent calls the gateway, not the other way around.
## Overview
The Tool Policy Hooks endpoint lets you enforce your organization's tool policies on agent tool calls. Before your agent executes a tool — whether it's a terminal command, an MCP tool, or any other action — your agent sends an **inbound** request to this endpoint and the gateway returns a decision. This is a request the agent makes to Unbound; Unbound does not call out to your agent.
Any agent framework or custom application can integrate with it.
Policies are configured at [gateway.getunbound.ai/policies/tool-policies](https://gateway.getunbound.ai/policies/tool-policies).
## Endpoint
```http theme={null}
POST /v1/hooks/pretool
```
## Authentication
Include your API key in the Authorization header:
```
Authorization: Bearer YOUR_API_KEY
```
## Request Body
| Parameter | Type | Required | Description |
| ------------------- | ------ | -------- | ------------------------------------------------------------------------------------------ |
| `conversation_id` | string | Yes | Unique identifier for the conversation or session |
| `model` | string | Yes | The model being used |
| `event_name` | string | Yes | `pre_tool_use` for tool policy checks, `user_prompt` for prompt guardrails |
| `unbound_app_label` | string | Yes | Your application identifier (e.g., `openclaw`, `claude-code`, `cursor`, or a custom label) |
| `pre_tool_use_data` | object | Yes | Details about the tool being invoked (see below) |
| `messages` | array | No | Conversation messages for context |
### pre\_tool\_use\_data Object
| Parameter | Type | Required | Description |
| ----------- | ------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `tool_name` | string | Yes | Name of the tool. Use `Bash`, `Shell`, or `exec` for terminal commands. For MCP tools, use the full name (e.g., `mcp__slack__send_message`) |
| `command` | string | Conditional | The shell command string. Required when `tool_name` is `Bash`, `Shell`, or `exec` |
| `metadata` | object | No | Additional metadata. For MCP tools, include `mcp_server` and `mcp_tool` |
## Response
| Field | Type | Description |
| ----------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `decision` | string | `allow`, `deny`, `ask`, or `approval_required` |
| `reason` | string | Present when `decision` is `deny` or `ask`. Human-readable explanation |
| `approvalCheck` | object | Present when `decision` is `approval_required`. Carries the `requestId` you poll with **and the `policyIds`** of the matching Require-Slack-Approval policies — send both back when polling (see [Checking approval status](#checking-approval-status)). When the request was escalated to an admin it also carries `escalatedToAdmin` and `escalatedAdminContact` so you can tell the user who to chase |
| `policy_check_failure_action` | string | Carries the action your organization has configured for the case where the policy check itself fails: `allow` (fail-open) or `block` (fail-closed). `allow`, `deny` and `ask` decisions all carry it, and so does a **503**. It is **absent** only from the `200 OK` fail-open response to a malformed or unauthenticated request. Honor it when the client cannot reach the gateway, and cache the last value you saw — see [Error responses](#error-responses). |
| `additionalContext` | string | Present on `deny` and `ask` responses when the matching policy supplies extra guidance for the client — a human-readable remediation hint or context string to surface alongside the approval prompt. Optional — absent when the policy has no extra context to add. |
### Decision Values
| Decision | Meaning | Recommended Action |
| ------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `allow` | Tool call is permitted | Proceed with execution |
| `deny` | Tool call is blocked by policy | Do not execute. Show the `reason` to the user |
| `ask` | Tool call requires user confirmation | Prompt the user for approval before executing. If your agent has no confirmation UI, treat as `allow` based on your security posture |
| `approval_required` | Tool call matched a Require Slack Approval policy. An approval request has been sent to Slack | Do not execute yet. Hold the call and poll [the approval-status endpoint](#checking-approval-status) until the approver decides |
## Checking approval status
When a tool call comes back as `approval_required`, an approver has been asked to allow or deny it in Slack. Hold the call and poll this endpoint until they answer.
```
POST https://api.getunbound.ai/v1/hooks/pretool/approval-status
```
Authenticate exactly as you do for the hook itself — `Authorization: Bearer `.
### Request
Send back both values you were given in `approvalCheck` — the `requestId` and the `policyIds`:
```json theme={null}
{
"requestId": "",
"policyIds": ["", ""]
}
```
| Field | Type | Required | Description |
| ----------- | --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `requestId` | string | Yes | The `requestId` from the `approvalCheck` object on the `approval_required` response |
| `policyIds` | string\[] | Recommended | The `policyIds` from the same `approvalCheck` object. With `requestId` alone, polling resolves the decision an approver made for **this specific request**. Passing `policyIds` too also catches a **blanket "allow for an hour" approval** an approver may have granted across the whole policy — omit them and that blanket approval isn't seen, so the call keeps returning `pending` until this request is answered directly or you hit your own timeout |
### Response
```json theme={null}
{ "decision": "pending" }
```
| Decision | Meaning | What to do |
| --------- | ----------------------- | ------------------------------------------- |
| `pending` | Nobody has answered yet | Keep holding, and poll again |
| `allow` | An approver allowed it | Execute the tool call |
| `deny` | An approver denied it | Do not execute. Tell the user it was denied |
`pending` is the safe default. If the request can't be resolved — a missing `requestId`, or a transient failure — the endpoint answers `pending` rather than guessing, so a held tool call is never released by accident.
That means **`pending` is not self-terminating.** Decide your own limit — a maximum number of polls, or a deadline — and choose what happens when you hit it. Treating a timeout as `deny` fails closed; treating it as `allow` fails open. Pick the one that matches your security posture, and surface the wait to your user so the agent doesn't just look hung.
## How It Works
The endpoint handles two types of tools differently:
**Terminal commands** (`Bash`, `Shell`, `exec`): The command is automatically classified and matched against your configured terminal command policies.
**MCP tools**: The `mcp_server` and `mcp_tool` from metadata are matched against your MCP tool policies.
**Other tools**: Tools that don't match a terminal command or MCP tool pattern return `allow`.
## Examples
### Terminal Command (Blocked)
```bash theme={null}
curl -X POST 'https://api.getunbound.ai/v1/hooks/pretool' \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "session-123",
"model": "claude-sonnet-4-6",
"event_name": "pre_tool_use",
"unbound_app_label": "my-agent",
"pre_tool_use_data": {
"tool_name": "Bash",
"command": "rm -rf /tmp/data",
"metadata": {}
},
"messages": [
{"role": "user", "content": "clean up temp files"}
]
}'
```
**Response:**
```json theme={null}
{
"decision": "deny",
"reason": "This command is blocked by your organization's policy.",
"policy_check_failure_action": "block"
}
```
### Terminal Command (Allowed)
```bash theme={null}
curl -X POST 'https://api.getunbound.ai/v1/hooks/pretool' \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "session-123",
"model": "claude-sonnet-4-6",
"event_name": "pre_tool_use",
"unbound_app_label": "my-agent",
"pre_tool_use_data": {
"tool_name": "Bash",
"command": "ls -la /tmp",
"metadata": {}
},
"messages": []
}'
```
**Response:**
```json theme={null}
{
"decision": "allow",
"policy_check_failure_action": "allow"
}
```
### MCP Tool
```bash theme={null}
curl -X POST 'https://api.getunbound.ai/v1/hooks/pretool' \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "session-123",
"model": "claude-sonnet-4-6",
"event_name": "pre_tool_use",
"unbound_app_label": "my-agent",
"pre_tool_use_data": {
"tool_name": "mcp__slack__send_message",
"command": "",
"metadata": {
"mcp_server": "slack",
"mcp_tool": "send_message"
}
},
"messages": []
}'
```
**Response** (depends on your configured policies):
```json theme={null}
{
"decision": "deny",
"reason": "This MCP tool call is blocked by your organization's policy.",
"policy_check_failure_action": "block"
}
```
## Error responses
The endpoint returns `200 OK` with `decision: "allow"` for requests that are malformed or unauthenticated — a fail-open posture. The response carries the decision **and nothing else**: `policy_check_failure_action` is absent, not set to a default.
```json theme={null}
{
"decision": "allow"
}
```
When policy evaluation itself fails, the endpoint returns **503**:
```json theme={null}
{
"error": "policy engine unavailable — please retry",
"policy_check_failure_action": "block"
}
```
A 503 **does** carry `policy_check_failure_action`, taken from your organization's configuration. Use it. The field is omitted only when the gateway could not resolve your configuration at all — in that case fall back to the last value you cached from a successful response.
If your client needs fail-closed behavior, gate it locally before calling the endpoint (validate the request body, confirm the API key is present) rather than relying on the gateway to reject.
**Treat an absent `policy_check_failure_action` as "no new information", never as a value.** The `200 OK` fail-open response omits the field entirely — it does not carry a default. A client that reads the omission as fail-open (for example, right after an API-key rotation) would silently apply fail-open behaviour on a fail-closed org. Keep the last value you received as authoritative, and never infer one from its absence.
This is the current behaviour, not an aspirational contract. Two more things worth knowing:
`/approval-status` is **fail-closed**: a missing or invalid API key returns **401**, the opposite of `/pretool`. The "pending is the safe default" guidance applies only after authentication succeeds.
Ignore response fields you don't recognise — new ones are added over time, the same rule that already applies to unknown `decision` values.
## Integration Guide
To integrate tool policies into your agent framework:
1. **Before each tool execution**, send a `POST` request to `/v1/hooks/pretool` with the tool details
2. **Check the `decision`** in the response:
* `allow` — execute the tool normally
* `deny` — block execution and surface the `reason` to the user
* `ask` — prompt the user for confirmation, or treat as `allow` if your agent has no interactive UI
* `approval_required` — do **not** execute. Hold the call and poll [the approval-status endpoint](#checking-approval-status) until the approver decides. See the full contract in [Decision Values](#decision-values)
3. **Treat an unrecognized `decision` as `deny`.** New decision values can be added over time; a client that falls through to "execute" on a value it doesn't know can bypass a guardrail. Default the unknown case to blocking, not allowing
4. **Handle errors gracefully** — if the endpoint is unreachable, decide whether to fail open (allow) or fail closed (deny) based on your security requirements
5. **Configure policies** at [gateway.getunbound.ai/policies/tool-policies](https://gateway.getunbound.ai/policies/tool-policies)
# Augment Code
Source: https://docs.getunbound.ai/integrations/augment-integration
Augment Code is an AI coding assistant built for large, complex codebases. Its context engine indexes your entire repository — architecture, dependencies, internal APIs, and naming conventions — to power chat, completions, and an agent that edits files, runs commands, and calls MCP tools. It works in the VS Code extension and the Auggie CLI.
## Prerequisites
Before setting up the integration, ensure you have:
* **Unbound CLI**: Installed and logged in — see the [CLI guide](/cli/overview)
* **Augment Code**: [VS Code extension](https://marketplace.visualstudio.com/items?itemName=augment.vscode-augment) or [Auggie CLI](https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli)
## Setup with Unbound CLI
```bash theme={null}
unbound setup augment
```
This installs the Unbound hooks for Augment Code. Both the VS Code extension and the Auggie CLI pick up the same hooks. Reload the VS Code window (and restart your terminal) after setup.
To remove the Unbound configuration:
```bash theme={null}
unbound setup augment --clear
```
## Usage
### Basic Usage
After configuration, every tool call Augment makes — shell commands, file reads, file writes, edits, and MCP actions — is intercepted by the Unbound hooks. Policies are checked before the tool runs, and matching actions (block, audit, Slack approval) are enforced inline.
## Security Benefits
Using Augment Code with Unbound Security AI Gateway provides:
* **Request Monitoring**: All Augment tool calls are logged and monitored
* **Analytics**: See which files and commands Augment touches across your org
* **Compliance**: Ensure AI interactions meet your organization's standards
* **Audit Trail**: Complete visibility into AI usage patterns
Install the CLI to set up and manage tools
Configure security guardrails for AI tools
# Claude Code
Source: https://docs.getunbound.ai/integrations/claude-code-integration
Learn how to install and configure Claude Code for secure AI-powered coding assistance
## What is Claude Code?
Claude Code is an agentic coding tool from Anthropic that runs directly in your terminal (or integrated in your IDE) and is designed to understand your full codebase, execute commands, edit files, create commits, and handle git workflows — all via natural-language prompts.
Its key capabilities include building new features from plain-English descriptions, debugging and fixing issues by analysing the codebase, navigating complex repositories, and automating repetitive engineering tasks.
## Integration Options
Unbound provides two ways to integrate Claude Code depending on your needs:
1. **Using API Key**: Use Claude Code through Unbound Gateway without needing your own Anthropic subscription
2. **With Anthropic Account**: Keep your existing Anthropic subscription and add Unbound for tracking and analytics
## Prerequisites
Before integration, ensure you have:
* **Unbound CLI**: Installed and logged in — see the [CLI guide](/cli/overview)
* **Claude Code**: [Install Claude Code](https://www.claude.com/product/claude-code)
## Setup with Unbound CLI
```bash theme={null}
unbound setup claude-code
```
You'll be prompted to choose a mode:
1. **Gateway** — Use Unbound as the AI provider (no Anthropic subscription needed)
2. **Subscription** — Keep your Anthropic subscription, add Unbound for policy enforcement and analytics
You can also skip the prompt:
```bash theme={null}
unbound setup claude-code --gateway # Use Unbound as AI provider
unbound setup claude-code --subscription # Hooks only (keep your subscription)
```
To remove the Unbound configuration:
```bash theme={null}
unbound setup claude-code --clear
```
After setup, restart your terminal and IDE, then run:
```bash theme={null}
claude
```
Install the CLI to set up and manage tools
Configure security guardrails for AI tools
# Cline
Source: https://docs.getunbound.ai/integrations/cline-integration
Learn how to configure Cline to use Unbound Security AI Gateway
## What is Cline?
Cline is an open source AI coding agent that brings frontier AI models directly to your IDE.
When integrated with the **Unbound Security AI Gateway**, all AI requests from Cline are securely routed through Unbound, enabling centralized guardrails, observability, and provider routing.
## Prerequisites
* **Unbound account** — Sign up at [gateway.getunbound.ai](https://gateway.getunbound.ai) if you don't have one
* **Cline extension** — Installed in VS Code or Cursor
## Get Your API Key
Go to [gateway.getunbound.ai/connect](https://gateway.getunbound.ai/connect) and copy your **API Key** and **Base URL**.
## Configuration
1. **Open Extensions** (Ctrl/Cmd + Shift + X)
2. **Search "Cline"** and **Install** the extension
3. **Open the Cline extension**
4. Select **"Bring your own API key"**
5. Set API Provider to **"OpenAI Compatible"**
6. **Base URL**: `https://api.getunbound.ai/v1`
7. **API Key**: Paste the API key from the connect page
8. **Select a model** (e.g., `gpt-4o`, `claude-sonnet-4-6`)
9. **Save & Continue**
## Usage
After configuration, Cline will automatically route all AI requests through the Unbound Gateway. All requests will be monitored, secured, and routed according to your organization's policy settings.
Install the CLI to set up and manage tools
Configure security guardrails for AI tools
# Codex
Source: https://docs.getunbound.ai/integrations/codex-integration
Codex is a powerful command-line interface (CLI) tool that provides AI-powered coding assistance directly from your terminal. It offers intelligent code suggestions, explanations, and debugging help to enhance your development workflow. By integrating Codex with Unbound Security AI Gateway, you can leverage these AI capabilities while maintaining enterprise-grade security, cost control, and compliance.
## Integration Options
Unbound provides two ways to integrate Codex depending on your needs:
1. **Using API Key**: Use Codex through Unbound Gateway without needing your own OpenAI subscription
2. **With OpenAI Account**: Keep your existing OpenAI subscription and add Unbound for tracking and analytics
## Prerequisites
Before setting up the integration, ensure you have:
* **Unbound CLI**: Installed and logged in — see the [CLI guide](/cli/overview)
* **Node.js**: Version 20 or higher
* **Codex CLI**: Install with `npm install -g @openai/codex`
## Setup with Unbound CLI
```bash theme={null}
unbound setup codex
```
You'll be prompted to choose a mode:
1. **Gateway** — Use Unbound as the AI provider (no OpenAI subscription needed)
2. **Subscription** — Keep your OpenAI subscription, add Unbound for policy enforcement and analytics
You can also skip the prompt:
```bash theme={null}
unbound setup codex --gateway # Use Unbound as AI provider
unbound setup codex --subscription # Hooks only (keep your subscription)
```
To remove the Unbound configuration:
```bash theme={null}
unbound setup codex --clear
```
After setup, restart your terminal. When you open Codex for the first time, select **"Provide your own API Key"** and press **Enter** — the key is already configured.
## Usage
### Basic Usage
Once configured, you can start using Codex in your terminal:
```bash theme={null}
codex
```
This launches the interactive Codex interface where you can:
* Ask coding questions
* Get code suggestions
* Request explanations for existing code
* Debug issues with AI assistance
### Example Commands
```bash theme={null}
# Start interactive mode
codex
# Ask a specific question
codex "How do I create a REST API in Node.js?"
# Get help with debugging
codex "Help me debug this Python function"
```
## Troubleshooting
### Common Issues
**Codex not found after installation:**
```bash theme={null}
# Check if npm global bin is in your PATH
npm config get prefix
# Add to PATH if needed
export PATH="$(npm config get prefix)/bin:$PATH"
```
**API key not recognized:**
```bash theme={null}
# Verify your environment variables
echo $OPENAI_API_KEY
echo $OPENAI_BASE_URL
# Once the setup is complete, open codex in a new terminal
```
**Connection issues:**
* Ensure your Unbound API key is valid
* Check that the base URL is correct
* Verify your internet connection
## Security Benefits
Using Codex with Unbound Security AI Gateway provides:
* **Request Monitoring**: All AI requests are logged and monitored
* **Cost Control**: Set spending limits and track usage
* **Compliance**: Ensure AI interactions meet your organization's standards
* **Guardrails**: Apply security policies to AI responses
* **Audit Trail**: Complete visibility into AI usage patterns
Install the CLI to set up and manage tools
Configure security guardrails for AI tools
# Anthropic Compliance API
Source: https://docs.getunbound.ai/integrations/compliance-api-integration
Import claude.ai chat activity into Unbound for organization-wide observability and audit trails
## What is the Compliance API?
The [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api) gives Claude Enterprise organizations programmatic access to their organization's retained activity — chats, files, projects, and users — for audit and governance. Unbound uses it to bring **claude.ai** usage into the same dashboards as your coding tools.
Where the [Cowork integration](/integrations/cowork-integration) streams telemetry to Unbound as it happens, the Compliance API is a pull: Unbound asks Anthropic for what has been retained since the last sync.
## What you get
* **Centralized observability** — claude.ai chats show up in your Unbound dashboards next to Cowork, Claude Code, Cursor, and every other integrated tool.
* **Audit trail** — a record of retained claude.ai activity for compliance reviews, attributed to the user who sent it.
* **One key, every organization** — a key created for a Claude Enterprise parent organization covers every linked organization beneath it.
## What Unbound imports
* **claude.ai chats** — prompts and responses, with the user they belong to.
* Chats appear under **Logs** with a tool type of **claude.ai**.
Unbound syncs **once an hour**, picking up chats created or updated since the previous run. This is not a live feed: a chat sent now shows up on the next sync, not within seconds.
## Prerequisites
* **Claude Enterprise** organization. The Compliance API is not available on Team plans, and a standalone Claude Console organization can reach the Activity Feed only.
* **Primary owner** of the parent organization, or an **organization owner** for a key covering a single organization.
* The Compliance API **enabled** for your organization at **claude.ai → Organization settings → API**. Enablement happens at the parent organization and cascades to every linked organization.
## Setup
### 1. Create a Compliance Access Key
1. Sign in to [claude.ai](https://claude.ai) as the primary owner or an organization owner.
2. Go to **Organization settings → API** and find the **Keys** section.
3. Click **Create key**, name it, and select both of these scopes:
| Scope | Grants |
| --------------------------- | ----------------------------------------------------------------- |
| `read:compliance_user_data` | Chats, messages, Cowork and Claude Code sessions, and user emails |
| `read:compliance_org_data` | Organization metadata — names, types, roles, and groups |
4. Click **Create**, then copy the key. It starts with `sk-ant-api01-` and is shown **only once**.
A Compliance Access Key can read every chat, file, project, and session transcript in every linked organization, including content the primary owner has not seen. Treat it like a production database credential. Do not grant `delete:compliance_user_data` — Unbound never deletes and does not need it.
Scopes are immutable after creation. To change them, create a new key and delete the old one.
### 2. Add the key in Unbound
1. Open the Unbound dashboard and go to **Settings → Integrations**.
2. On the **Anthropic** card, click **Connect**.
3. Paste the Compliance Access Key and click **Connect**. Unbound checks the key immediately by listing a single chat, so a key without `read:compliance_user_data` is rejected on the spot.
## Verification
1. After connecting, the **Anthropic** card shows **Connected**. Click the card to open its panel, which shows the **Last synced** time once the first sync completes.
2. Open **Logs** and filter by the **claude.ai** tool type — your chats appear there.
If nothing shows up:
* Give it up to an hour. The sync is hourly, so a chat sent just now waits for the next run. To pull immediately, click **Sync now** in the Anthropic panel.
* Confirm the key also carries `read:compliance_org_data`. Only `read:compliance_user_data` is exercised when you connect, so a key missing the organization scope is accepted but cannot enumerate your linked organizations.
* Confirm the Compliance API is still **enabled** in claude.ai. While it is off, no new activity is recorded, and activity from that period cannot be recovered later.
* Confirm the key has not been deleted in claude.ai. Deletion takes effect on the next request, with no grace period.
# GitHub Copilot
Source: https://docs.getunbound.ai/integrations/copilot-integration
GitHub Copilot is GitHub's AI pair programmer that suggests code completions, explains code, and runs agentic workflows across your editor and terminal. It works in the VS Code extension and the `copilot` CLI.
## Prerequisites
Before setting up the integration, ensure you have:
* **Unbound CLI**: Installed and logged in — see the [CLI guide](/cli/overview)
* **GitHub Copilot**: [VS Code extension](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot) or [Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli/about-github-copilot-in-the-cli)
## Setup with Unbound CLI
```bash theme={null}
unbound setup copilot
```
This installs the Unbound hooks for Copilot. Both the VS Code extension and the `copilot` CLI pick up the same hooks. Restart your terminal (and reload the VS Code window) after setup.
To remove the Unbound configuration:
```bash theme={null}
unbound setup copilot --clear
```
## Usage
### Basic Usage
After configuration, every tool call Copilot makes — shell commands, file reads, file writes, edits — is intercepted by the Unbound hooks. Policies are checked before the tool runs, and matching actions (block, warn, audit, Slack approval) are enforced inline.
## Security Benefits
Using GitHub Copilot with Unbound Security AI Gateway provides:
* **Request Monitoring**: All Copilot tool calls are logged and monitored
* **Analytics**: See which files and commands Copilot touches across your org
* **Compliance**: Ensure AI interactions meet your organization's standards
* **Audit Trail**: Complete visibility into AI usage patterns
Install the CLI to set up and manage tools
Configure security guardrails for AI tools
# Cowork
Source: https://docs.getunbound.ai/integrations/cowork-integration
Stream Claude Cowork telemetry through Unbound for organization-wide observability, cost tracking, and policy enforcement
## What is Cowork?
[Claude Cowork](https://www.anthropic.com/product/claude-cowork) is Anthropic's agentic AI for knowledge work, built into the [Claude desktop app](https://support.anthropic.com/en/articles/10065433-installing-claude-for-desktop). It runs across local files, folders, and the applications people use every day, synthesizing information and completing multi-step tasks without the user coordinating each action.
## What you get
* **Centralized observability** — every Cowork session shows up in your Unbound dashboards next to Claude Code, Cursor, Codex, and every other integrated tool.
* **Cost attribution** — tie spend back to users, teams, and projects through Unbound.
* **Audit trail** — a complete record of Cowork activity for compliance reviews.
## Prerequisites
* **Unbound account** with an API key. Generate one from **Connect → AI Coding Tools → Cowork** in the Unbound dashboard.
* **Claude desktop app**, version **1.1.4173 or later**, signed in on a **Team** or **Enterprise** plan.
* If your network has egress restrictions, add your Unbound gateway domain to **Organization settings → Capabilities → Network egress** in the Claude desktop app.
## Setup
1. Open the **Claude desktop app**.
2. Go to **Organization settings → Cowork → Monitoring**.
3. Fill in the following fields:
| Field | Value |
| ------------- | --------------------------------------------- |
| OTLP endpoint | `https://api.getunbound.ai/otel` |
| OTLP protocol | `http/json` |
| OTLP headers | `Authorization=Bearer ` |
4. Save. Users in your organization must **start a new Cowork session** for the change to take effect — settings are loaded at session start, not on the fly.
Copy-ready values and your account's API key are available in the Unbound dashboard at **Connect → AI Coding Tools → Cowork**.
## Verification
1. Start a new Cowork session in the Claude desktop app and run any task.
2. Open the Unbound dashboard and check **Logs** — you should see a new Cowork event within a few seconds.
3. Cost and usage for Cowork appear under **Analytics** alongside your other integrated tools.
If nothing shows up:
* Confirm the user **started a new session** after the setting was saved. Cowork does not hot-reload the monitoring config.
* Confirm the API key in the `Authorization` header is valid and not revoked.
* If your network restricts egress, confirm your Unbound gateway domain is whitelisted under **Capabilities → Network egress**.
* Confirm the desktop app version is **1.1.4173 or later**. Older builds don't support OTLP export.
# Cursor
Source: https://docs.getunbound.ai/integrations/cursor-integration
Cursor IDE is an AI-first code editor that deeply understands your whole codebase to offer natural-language coding, multi-file refactoring, and smart autocomplete. It blends project-level context with LLM-powered agents to speed up development while letting you review exactly what changes before applying them.
## Prerequisites
Before setting up the integration, ensure you have:
* **Unbound CLI**: Installed and logged in — see the [CLI guide](/cli/overview)
* **Cursor**: [Download Cursor](https://cursor.com/download)
## Setup with Unbound CLI
```bash theme={null}
unbound setup cursor
```
This will configure Cursor to route all AI requests through Unbound. Cursor will restart automatically.
To remove the Unbound configuration:
```bash theme={null}
unbound setup cursor --clear
```
## Usage
### Basic Usage
After configuration, Cursor will automatically route completions and AI actions through the Unbound Gateway.
## Security Benefits
Using Cursor with Unbound Security AI Gateway provides:
* **Request Monitoring**: All AI requests are logged and monitored
* **Analytics**: See which files are most touched by AI through coding agents
* **Compliance**: Ensure AI interactions meet your organization's standards
* **Audit Trail**: Complete visibility into AI usage patterns
Install the CLI to set up and manage tools
Configure security guardrails for AI tools
# Direct Install
Source: https://docs.getunbound.ai/integrations/direct-install
Install a tool without the CLI, or onto a custom Unbound domain
`unbound setup ` is the normal path. Use the direct installer when the Unbound CLI isn't on the machine, or when your organization runs Unbound on its own hosts.
**macOS and Linux**
```bash theme={null}
python3 <(curl -fsSL https://getunbound.ai/setup/claude-code/gateway/install) --domain gateway.getunbound.ai
```
**Windows** — PowerShell has no process substitution, so download the installer first and run it:
```powershell theme={null}
$installer = Join-Path $env:TEMP 'unbound-install.py'
Invoke-WebRequest -Uri 'https://getunbound.ai/setup/claude-code/gateway/install' -OutFile $installer -UseBasicParsing
python $installer --domain gateway.getunbound.ai
Remove-Item $installer -Force
```
If `python` isn't on your `PATH`, use `py -3` in its place.
Replace `gateway.getunbound.ai` with your organization's Unbound dashboard host, and the route with the one for your tool from the table below.
## Routes
| Tool | Route |
| ------------------------------- | ------------------------------------ |
| Claude Code (API key) | `/setup/claude-code/gateway/install` |
| Claude Code (Anthropic account) | `/setup/claude-code/hooks/install` |
| Cursor | `/setup/cursor/install` |
| Codex (API key) | `/setup/codex/gateway/install` |
| Codex (OpenAI account) | `/setup/codex/hooks/install` |
| GitHub Copilot | `/setup/copilot/hooks/install` |
| Augment Code | `/setup/augment/hooks/install` |
| Gemini CLI | `/setup/gemini-cli/gateway/install` |
Run these **without** `sudo` — they configure the account you run them as. For fleet-wide deployment see [Deploy AI Tools via MDM](/mdm-integrations/deploy-ai-tools).
## Flags
| Flag | Purpose | Default |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| `--domain ` | Your Unbound **dashboard** host. The installer opens it in a browser to fetch your API key. Required unless you pass `--api-key` | — |
| `--api-key ` | Use a key you already have and skip the browser step | — |
| `--backend-url ` | Your Unbound backend, if your organization runs its own | `https://backend.getunbound.ai` |
| `--gateway-url ` | The AI gateway your tool is pointed at, if your organization runs its own | `https://api.getunbound.ai` |
| `--clear` | Remove this tool's Unbound configuration. Needs no key and no URLs | — |
| `--backfill` | Send recent local session history on install, so the dashboard isn't empty on day one. Backfills for **Claude Code hooks, Codex hooks and Copilot**. Cursor and Augment accept the flag and print *backfill is not supported* — they keep no transcripts on disk. The gateway installers ignore it | off |
| `--debug` | Verbose output | off |
| `--config-dir ` | Claude Code only: the Claude config directory. Must match `CLAUDE_CONFIG_DIR` in your environment, or Claude Code won't read the install | `$CLAUDE_CONFIG_DIR`, else `~/.claude` |
`--domain` only controls **where the browser goes to authenticate**. It does not move your model traffic.
If your organization runs its own Unbound backend or gateway, pass `--backend-url` and `--gateway-url` as well. Passing `--domain` alone leaves the tool pointed at Unbound's default hosts, and everything appears to install correctly.
Pass values in the `--flag value` form, with a space. The **hooks** installers — Claude Code hooks, Codex hooks, Cursor, Copilot, Augment — scan the argument list for exact flag names, so `--domain=value`, `--api-key=value` and the rest are read as unknown arguments and silently ignored. On its own that stops the run with `Missing required argument: --domain or --api-key` even though you passed it, and alongside `--api-key` it installs successfully but records no dashboard host. `--config-dir` is the one exception there and accepts either form. The **gateway** installers parse arguments properly and take either form — but use the space form everywhere and you never have to remember which is which.
These installers have no `--help` — the table above is the flag list.
## On a managed device
On an MDM-managed device the Claude Code, Codex, Cursor and Augment installers stop and change nothing, naming the tool — `Skipped — Claude Code is managed by your organization (MDM)`, and likewise for the other three. They exit with status 3. The Copilot installer has no such guard and will install over an MDM deployment. That's expected: your MDM deployment is already in place and a per-user install can't override it.
## If the browser step doesn't finish
The installer waits for a callback from the dashboard you named in `--domain`. If it hangs, or ends with `No api_key found in callback. Exiting.`, the host you passed isn't the dashboard that can mint a key for your organization — check it against the URL you use to sign in.
# Gemini CLI
Source: https://docs.getunbound.ai/integrations/gemini-cli-integration
Gemini CLI is a powerful command-line interface tool that provides direct access to Google's Gemini AI models from your terminal. It offers intelligent code generation, natural language processing, and development assistance capabilities. By integrating Gemini CLI with Unbound Security AI Gateway, you can leverage these AI capabilities while maintaining enterprise-grade security, cost control, and compliance.
## Prerequisites
Before setting up the integration, ensure you have:
* **Unbound CLI**: Installed and logged in — see the [CLI guide](/cli/overview)
* **Node.js**: Version 20 or higher
* **Gemini CLI**: Install with `npm install -g @google/gemini-cli`
## Setup with Unbound CLI
```bash theme={null}
unbound setup gemini-cli
```
This sets the `GEMINI_API_KEY` and `GOOGLE_GEMINI_BASE_URL` environment variables to route Gemini CLI through Unbound.
To remove the Unbound configuration:
```bash theme={null}
unbound setup gemini-cli --clear
```
Restart your terminal after setup. When Gemini CLI prompts for authentication, select **"Use Gemini API Key"** and press **Enter** — the key is already configured.
## Usage
### Basic Usage
Once configured, you can start using Gemini CLI in your terminal:
```bash theme={null}
gemini
```
This launches the interactive Gemini interface where you can:
* Generate code for various programming languages
* Ask questions about development topics
* Get assistance with debugging
* Process and analyze text content
### Example Commands
```bash theme={null}
# Start interactive mode
gemini
# Generate code for a specific task
gemini "Create a Python function to sort a list"
# Ask development questions
gemini "How do I implement authentication in a web app?"
# Get help with debugging
gemini "Help me debug this JavaScript error"
```
### Advanced Usage
```bash theme={null}
# Generate code with specific parameters
gemini --model="gemini-pro" "Write a REST API in Node.js"
# Process files
gemini --input="code.py" "Explain this code"
# Generate documentation
gemini "Generate documentation for this function"
```
### Model Selection
You can specify different Gemini models:
```bash theme={null}
# Use specific model
gemini --model="gemini-pro" "Your prompt here"
# List available models
gemini --list-models
```
## Troubleshooting
### Common Issues
**Gemini CLI not found after installation:**
```bash theme={null}
# Check if npm global bin is in your PATH
npm config get prefix
# Add to PATH if needed
export PATH="$(npm config get prefix)/bin:$PATH"
```
**API key not recognized:**
```bash theme={null}
# Verify your environment variables
echo $GEMINI_API_KEY
echo $GOOGLE_GEMINI_BASE_URL
# Once the setup is complete, open gemini-cli in a new terminal
```
**Connection issues:**
* Ensure your Unbound API key is valid
* Check that the base URL is correct
* Verify your internet connection
* Confirm the Unbound service is running
**Model not found:**
```bash theme={null}
# Check available models
gemini --list-models
# Use a different model if needed
gemini --model="gemini-pro-vision" "Your prompt"
```
## Security Benefits
Using Gemini CLI with Unbound Security AI Gateway provides:
* **Request Monitoring**: All AI requests are logged and monitored
* **Cost Control**: Set spending limits and track usage across all models
* **Compliance**: Ensure AI interactions meet your organization's standards
* **Guardrails**: Apply security policies to AI responses
* **Audit Trail**: Complete visibility into AI usage patterns
* **Model Routing**: Intelligent routing to optimize performance and costs
* **Rate Limiting**: Prevent abuse and manage resource usage
## Best Practices
### Development Workflow
1. **Start with simple prompts** to test the integration
2. **Use specific, clear instructions** for better results
3. **Iterate on prompts** to improve output quality
4. **Monitor usage** through Unbound dashboard
5. **Set appropriate guardrails** for your use case
### Security Considerations
* Keep your API keys secure and never commit them to version control
* Use environment variables for configuration
* Regularly rotate your API keys
* Monitor usage patterns for anomalies
* Set up appropriate guardrails for your organization's needs
Install the CLI to set up and manage tools
Configure security guardrails for AI tools
# Kilo Code
Source: https://docs.getunbound.ai/integrations/kilo-integration
Learn how to configure Kilo Code to use Unbound Security AI Gateway
## What is Kilo Code?
Kilo Code is an all-in-one agentic engineering platform that brings AI-powered coding assistance directly to your IDE and CLI.
When integrated with the **Unbound Security AI Gateway**, all AI requests from Kilo Code are securely routed through Unbound, enabling centralized guardrails, observability, and provider routing.
## Prerequisites
* **Unbound account** — Sign up at [gateway.getunbound.ai](https://gateway.getunbound.ai) if you don't have one
* **Kilo Code extension** installed in your IDE, or **Node.js 20+** for the CLI
## Get Your API Key
Go to [gateway.getunbound.ai/connect](https://gateway.getunbound.ai/connect) and copy your **API Key**.
## IDE Extension Setup
1. **Open Extensions** (Ctrl/Cmd + Shift + X)
2. **Search "Kilo Code"** and **Install** the extension
3. **Open the Kilo extension**
4. Select **"Use your own API key"**
5. Set API Provider to **"OpenAI Compatible"**
6. **Base URL**: `https://api.getunbound.ai/v1`
7. **API Key**: Paste the API key from the connect page
8. **Select a model** (e.g., `claude-opus-4-5`, `gpt-5.1`)
9. **Save & Continue**
## CLI Setup
1. Install: `npm install -g @kilocode/cli`
2. Run: `kilocode`
3. Set API Provider to **"Unbound"**
4. Paste your API key
5. Select a model
## Usage
After configuration, Kilo Code will automatically route all AI requests through the Unbound Gateway. All requests will be monitored, secured, and routed according to your organization's policy settings.
Install the CLI to set up and manage tools
Configure security guardrails for AI tools
# Roo Code
Source: https://docs.getunbound.ai/integrations/roo-code-integration
Learn how to configure Roo Code to use Unbound Security AI Gateway for secure AI-powered coding
## What is Roo Code?
Roo Code is an open-source AI-powered coding assistant that helps developers write, review, and debug code more efficiently. When integrated with the Unbound Security AI Gateway, all AI requests are securely routed through Unbound, enabling centralized guardrails, observability, and provider routing.
## Prerequisites
* **Unbound account** — Sign up at [gateway.getunbound.ai](https://gateway.getunbound.ai) if you don't have one
* **Roo Code** — Installed in VS Code or Cursor
## Get Your API Key
Go to [gateway.getunbound.ai/connect](https://gateway.getunbound.ai/connect) and copy your **API Key**.
## Configuration
1. **Open Command Palette** (Ctrl/Cmd + Shift + P)
2. **Type "Roo Code: Open Settings"** and select it
3. **Select "unbound"** as the API provider
4. **Paste your API key** into the "Unbound API Key" text box
5. **Select your preferred model** from the dropdown
6. **Click "Done"** to save your settings
Install the CLI to set up and manage tools
Configure security guardrails for AI tools
# Slack
Source: https://docs.getunbound.ai/integrations/slack
Connect Slack to enable interactive approval workflows for tool policies
Connecting Slack to Unbound enables the **Require Slack Approval** policy action. When an agent runs a command that matches this policy, an interactive Slack DM is sent to the developer. They can approve or deny the action without leaving Slack, and the agent retries automatically once a decision is made.
## Prerequisites
* **Unbound account** with Admin role
* **Slack workspace** where you have permission to install apps
## Setup
1. Go to [Settings → Integrations](https://gateway.getunbound.ai/settings?tab=integrations) in the Unbound dashboard
2. Find **Slack** and click **Connect**
3. Complete the Slack OAuth flow to authorize the Unbound app for your workspace
4. Once authorized, the integration status shows as **Active**
## How the Approval Flow Works
When an agent triggers a **Require Slack Approval** policy:
1. The tool call is held in a pending state
2. A Slack DM is sent to the developer who owns the AI tool, matched by email address
3. The DM shows the matched policy, the command preview, and three action buttons:
* **Allow Once** — approves this specific request (valid for 10 minutes)
* **Allow 1 hour** — approves all matching commands from this tool for 60 minutes
* **Deny** — rejects this specific request
4. The agent receives the decision and either proceeds or surfaces the denial to the user
If the developer's Unbound email doesn't match a Slack account, the DM falls back to an org admin. If no admin can be reached via Slack, the request is automatically denied to preserve safety.
## Creating a Require Slack Approval Policy
1. Go to [Tool Policies](https://gateway.getunbound.ai/policies/tool-policies) and click **Create Policy**
2. Select **Terminal Commands** or **MCP Actions**
3. Configure the command family or MCP tool to monitor
4. Set **Action** to **Require Slack Approval**
5. Optionally scope to specific user groups or AI tools
6. Click **Create Policy**
The Slack integration must be active before policies with this action will fire. Policies created before Slack is connected will start working immediately once the integration is set up.
Configure the Require Slack Approval action
Create Slack approval policies from the terminal
# Webhooks
Source: https://docs.getunbound.ai/integrations/webhooks
Receive HTTP notifications at your own URL for subscribed events
Webhooks let you receive real-time notifications at your own URL when subscribed events occur. Use them to forward events to your SIEM, build approval workflows, notify on-call, or trigger automations.
## Prerequisites
* **Unbound account** with Admin role
* A **public HTTPS endpoint** you control to receive the events
* (Optional) A **server-side secret manager** to store the signing secret
## Setup
1. In your Unbound dashboard, go to **Settings → Webhooks**
2. Click **Add Endpoint**
3. Enter your endpoint URL (must be `https://`, public, and not pointing at a private network)
4. Optionally add a description
5. Select one or more events to subscribe to (or **Select all**)
6. Click **Create**
On creation, your endpoint's `whsec_…` signing secret is shown **once**. Copy it now and store it in your secret manager. You can reveal it again from the endpoint detail page if you need to, but treat it like an API key.
## Signature verification
Every delivery includes three headers your receiver can use to verify the event came from Unbound and wasn't tampered with in transit.
| Header | Description |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `webhook-id` | Unique idempotency key (`msg_…`). Use it to deduplicate retries. |
| `webhook-timestamp` | Unix seconds when we signed the payload. Reject deliveries more than \~5 minutes old to prevent replay. |
| `webhook-signature` | `v1,` computed over `${webhook-id}.${webhook-timestamp}.${raw-body}` using your signing secret as the HMAC key. |
The verification algorithm is standard HMAC-SHA256:
1. Strip the `whsec_` prefix from your signing secret and base64-decode the remainder. The result is the HMAC key.
2. Build the signed payload by concatenating `..`.
3. Compute `HMAC-SHA256(key, signed_payload)` and base64-encode the digest. Prefix it with `v1,`.
4. Compare against the `webhook-signature` header using a constant-time comparison. Multiple signatures may be space-separated; accept if any one matches.
5. Reject deliveries whose `webhook-timestamp` is older than 5 minutes to prevent replay.
Always verify against the **raw request body bytes**, not a re-serialised JSON object. JSON re-serialisation can reorder keys or change whitespace, breaking the signature.
## Event types
Each command or tool call can fire multiple events. The four per-action events fire whenever a policy of that action matches, and `*.logged` fires for everything. A command that matched a Block policy therefore produces two deliveries: `*.blocked` and `*.logged`.
| Event | Fires when |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `terminal_command.blocked` | A terminal command matched a Block policy |
| `terminal_command.warned` | A terminal command matched a Warn policy |
| `terminal_command.slack_approval_requested` | A terminal command is awaiting Slack approval |
| `terminal_command.audited` | A terminal command matched an Audit policy |
| `terminal_command.logged` | Every terminal command observed by Unbound |
| `mcp_tool.blocked` | An MCP tool call matched a Block policy |
| `mcp_tool.warned` | An MCP tool call matched a Warn policy |
| `mcp_tool.slack_approval_requested` | An MCP tool call is awaiting Slack approval |
| `mcp_tool.audited` | An MCP tool call matched an Audit policy |
| `mcp_tool.logged` | Every MCP tool call observed by Unbound |
| `secrets.blocked` | A request was blocked by a Secrets policy |
| `secrets.audited` | A request matched an audit-only Secrets policy |
| `ai_tool.unsanctioned_detected` | An unsanctioned AI coding tool was found on a user's device, the first time it's seen for that user |
| `ai_tool.unsanctioned_used` | A user used an unsanctioned AI coding tool. Rate-limited to at most once every 24 hours per user and tool, so it won't fire on every request |
You can subscribe to specific events, an entire group (`terminal_command.*`, `mcp_tool.*`, `secrets.*`, or `ai_tool.*`), or all events (`*`). `*.logged` is the firehose — every classified command or tool call. Use it for SIEM streaming; pick the per-action events if you only want policy-driven activity.
The `ai_tool.*` events are your Shadow AI feed: they fire when a tool that isn't on your sanctioned list turns up in the fleet. Route them somewhere a human will see them.
## Payload
Every event uses the same envelope: a top-level `id` (ULID, prefixed `msg_`), `type` (the event name), `timestamp` (ISO-8601, UTC), and a nested `data` block. The `data` block is consistent across all event types within a family — terminal-command events carry a `command` string, MCP-tool events carry `mcp_server`, `mcp_tool`, and `mcp_parameters` instead, and everything else is shared.
A single command or tool call can trigger multiple events (e.g. a blocked command fires both `*.blocked` and `*.logged`). Each event is a separate delivery with its own `id` and signature, but they share the same `data.tool_use_id` so you can dedupe across events if you want one record per command. Secrets events (`secrets.blocked` / `secrets.audited`) don't carry `tool_use_id` — use `request_id` to dedupe those.
Shadow AI events (`ai_tool.*`) describe a **tool on a device**, not a single command, so they carry their own set of fields — see [Shadow AI events](#shadow-ai-events) below. They don't carry `tool_use_id`, and there's no per-command grouping to do: each delivery is a distinct alert. Deduplicate retries by `webhook-id` like any other event.
### Field reference
| Field | Type | Description |
| -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tool_use_id` | string | Stable per-command identifier (prefixed `tu_`). The same value appears on every event fired for the same command — use it to dedupe across overlapping subscriptions (e.g. an endpoint subscribed to both `*.blocked` and `*.logged` receives two deliveries with the same `tool_use_id`). |
| `tool` | string \| null | The AI tool that issued the call (e.g. `claude-code`, `cursor`, `codex`, `copilot`). |
| `tool_name` | string | The specific tool name. For terminal commands this is `Bash`, `Edit`, etc.; for MCP calls it's `__`. |
| `user_email` | string \| null | The email of the user whose AI tool fired the call. |
| `prompt` | string \| null | The user's original prompt that led to this activity. Useful for context. |
| `thread_id` | string \| null | Conversation thread ID for grouping related events. |
| `intent_attribution` | string | `USER_INTENTIONAL`, `AGENT_INITIATED`, or `UNKNOWN` — whether the user explicitly asked for this or the agent decided autonomously. |
| `command` | string | (terminal only) The literal command string. |
| `mcp_server` | string | (MCP only) The MCP server the tool call targeted. |
| `mcp_tool` | string | (MCP only) The specific tool within that server. |
| `mcp_parameters` | object | (MCP only) The parsed arguments passed to the tool. |
| `matched_policies` | array | Every policy that matched this event. Empty on `*.logged` events when no policy matched. Each entry contains `id`, `name`, `action`, `policy_type`. |
| `classifications` | array | All command families this event was classified into (compound commands can produce multiple). Each entry has `command_family`, `confidence_score`, `targets`. |
| `guardrail` | string | (secrets only) The DLP guardrail that matched — always `Secrets`. |
| `action` | string | (secrets only) `BLOCK` or `AUDIT`. |
| `model` | string \| null | (secrets only) The model the request targeted. |
| `request_id` | string \| null | (secrets only) The gateway request identifier. |
| `detections` | array | (secrets only) Each detected secret. Each entry has `type` (`API Key`, `Cryptographic Key`, or `Database Connection String`) and `confidence`. |
| `detection_count` | number | (secrets only) Number of entries in `detections`. |
| `user_prompt` | string \| null | (secrets only) The user's prompt that triggered the detection. May contain the flagged value. |
| `log_url` | string | (secrets only) Deep link to the matching entry in the Data Security log. |
### Shadow AI events
`ai_tool.unsanctioned_detected` and `ai_tool.unsanctioned_used` report an unsanctioned tool on a user's device, so their `data` block is different from the command events above.
```json theme={null}
{
"id": "msg_01J9Z...",
"type": "ai_tool.unsanctioned_detected",
"timestamp": "2026-07-02T12:00:00Z",
"data": {
"tool_name": "Cursor",
"app_label": "cursor",
"sanctioned": false,
"organization_id": 123,
"user_id": 4567,
"user_full_name": "Vignesh Subbiah",
"device_id": 8910,
"detection_count": 1,
"first_detected_at": "2026-07-02T12:00:00+00:00",
"last_detected_at": "2026-07-02T12:00:00+00:00",
"source": "gateway_runtime",
"request_id": "req_...",
"model": "claude-opus-4-8"
}
}
```
| Field | Type | Description |
| ------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `tool_name` | string | The unsanctioned tool that was found (e.g. `Cursor`). |
| `app_label` | string | The tool's identifier (e.g. `cursor`). |
| `sanctioned` | boolean | Always `false` on these events — that's why they fired. |
| `user_id` | number | The user the tool belongs to. Present on every event; use it to route the alert even when you have no email. |
| `user_full_name` | string | The user's name, for display in an alert. |
| `device_id` | number \| null | The device the tool was found on. Use it to route the alert to the affected endpoint. `null` if the device couldn't be resolved. |
| `detection_count` | number | How many times this tool has been seen for this user. `1` on first detection, and climbing on `unsanctioned_used`. |
| `first_detected_at` | string | When this tool was first seen for this user (ISO-8601, UTC). |
| `last_detected_at` | string | When it was most recently seen (ISO-8601, UTC). |
| `organization_id` | number | Your organization. |
| `source` | string | Where the sighting came from. |
| `request_id` | string \| null | The gateway request that surfaced it, when there was one. |
| `model` | string \| null | The model the tool was calling, when known. |
**Deduplicating.** To drop duplicate **deliveries** of the same event, use `webhook-id` — the idempotency key on every delivery, exactly as you would for any other event.
Do **not** build a longer-lived idempotency key out of `user_id` + `tool_name`. These events are deliberately recurring: `unsanctioned_detected` fires **once**, the first time a tool is seen for a user, and `unsanctioned_used` fires **at most once every 24 hours** for the same user and tool for as long as they keep using it. Two `unsanctioned_used` deliveries share the same `user_id` and `tool_name` but are **different alerts** — collapsing on those fields would suppress the recurring-usage signal, which is the point of the event. They carry no `tool_use_id` and are not a per-command feed; treat each delivery as its own alert and key only retries on `webhook-id`.
## Retries
Failed deliveries (anything that isn't HTTP 200–299) retry on the following schedule:
| Attempt | Delay since previous |
| ------- | -------------------- |
| 1 | (immediate) |
| 2 | 5 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 5 hours |
| 7 | 10 hours |
| 8 | 10 hours |
The retry horizon is approximately 28 hours total. Each attempt times out after **15 seconds**. After 8 failed attempts the event is dropped.
Redirects (`3xx`) are treated as failures and **not followed**. Configure your endpoint to be the resolved URL.
## Custom headers
You can attach static custom headers to every delivery for your endpoint — useful when your receiver requires a specific authentication header.
| Receiver | Header |
| ---------------------- | ------------------------------------------ |
| Splunk HEC | `Authorization: Splunk ` |
| Datadog Logs Intake | `DD-API-KEY: ` |
| Sumo Logic HTTP Source | (no header — URL is the credential) |
| Tines | (no header — URL secret is the credential) |
| Slack incoming webhook | (no header — URL is the credential) |
To add custom headers, click your endpoint in the dashboard, then **Custom headers → Edit**. Reserved headers (`content-type`, `user-agent`, `host`, and our signature headers) cannot be overridden.
## Testing
To send a test event:
1. Open your endpoint from **Settings → Webhooks**
2. Click **Send test event**
3. Pick an event type from the dropdown
4. Click **Send**
The test event uses the same signing pipeline and headers as a real event. Failed test deliveries are **not retried** so you can iterate quickly on your receiver. Test deliveries appear in the **Message Attempts** table tagged with `test`.
## Managing endpoints
From the endpoint detail page you can:
* **Edit subscribed events** — change which events trigger this endpoint
* **Edit description** — update the human-readable label
* **Edit custom headers** — change static headers sent on every delivery
* **Disable** — stop firing without losing history (use the three-dot menu)
* **Delete** — remove the endpoint and its delivery history permanently
The **Success rate (24h)** column on the endpoints list shows the percentage of HTTP 2xx responses in the last 24 hours. A dash means no deliveries in that window.
## Troubleshooting
Verify the endpoint is **Enabled**, the events you expect are in **Subscribed events**, and the URL is reachable from the public internet. Use **Send test event** to confirm end-to-end connectivity. If you want to confirm any traffic is flowing, subscribe to `terminal_command.logged` and `mcp_tool.logged` — the firehose events fire for every classified call regardless of risk or policy match. Then double-check the Terminal Runs table on the [Tool Use](https://gateway.getunbound.ai/analytics/tool-use) page for the activity you expected.
The most common cause is verifying against a parsed JSON object instead of the raw body bytes. Make sure your handler reads the raw request body before parsing. Also confirm you copied the full `whsec_…` secret without truncation.
Process the event asynchronously. Acknowledge with 200 immediately and queue the actual work — anything taking longer than 15 seconds will be treated as a failure and retried.
Delivery history is visible per-endpoint, but events are not currently replayable through the UI. Re-trigger the source action in your AI tool, or contact support to manually replay a specific delivery.
Configure which actions trigger which event types
Pair webhooks with Slack approval workflows
# Introduction
Source: https://docs.getunbound.ai/introduction
Learn about Unbound Security, the Agent Access Security Broker for AI coding agents
## What is Unbound Security?
Unbound Security is an **Agent Access Security Broker (AASB)** that helps organizations discover, assess, and enforce policy for AI coding agents. Unbound gives security teams full visibility and control over every agent, tool, and action — covering terminal commands, file access, MCP connections, and IDE plugins across tools like Claude Code, Cursor, Cline, GitHub Copilot, and Windsurf.
## Key Capabilities
### Agent Discovery
* **Inventory AI coding agents in use**: Detect Claude Code, Cursor, Windsurf, GitHub Copilot, Cline, and more across your engineering org
* **Enumerate MCP servers**: Surface every MCP server a developer has connected, including risky or unknown ones
* **Map IDE plugins**: See which AI-powered IDE extensions are installed and how they are configured
### Risk Assessment
* **Per-developer posture scores**: A live security signal for each engineer based on the agents, plugins, and MCP servers they have enabled
* **Risky MCP connection alerts**: Flag MCP servers with excessive permissions or untrusted sources
* **Autonomy analysis**: Understand how much runway each agent has inside each developer's environment
### Policy Enforcement
* **Terminal command control**: Allow or deny commands with semantic parsing, not brittle regex
* **MCP policies**: Govern which MCP servers and tools each agent can reach
* **Approval workflows**: Require human approval for sensitive agent actions
* **Audit logging**: A complete record of what every agent did, for compliance and incident review
### Data Protection
* **Automatic secret detection**: Catch and redact secrets that would otherwise leak to LLM providers
* **Sensitive data routing**: Route requests containing sensitive content to private LLMs in Google Vertex AI, Bedrock, or confidential computing infrastructure
* **Your data stays private**: While keeping your teams productive
## How It Works
Unbound operates as an endpoint-level control plane for AI coding agents. Three steps:
### 1. Discover
Unbound inventories the AI coding agents, MCP servers, and IDE plugins in use across your engineering org — no agent-by-agent configuration required.
### 2. Assess
Every agent, tool, and action is scored for risk. Security teams get a live posture view: who is running what, which MCP servers are risky, and where policies are missing.
### 3. Enforce
Policies are enforced at the endpoint and at the model layer — terminal command allow/deny, MCP scope controls, approval workflows, and optional LLM request routing for sensitive data.
## Supported Integrations
Unbound governs and integrates with popular AI coding tools and platforms:
* **AI Coding Agents**: Claude Code, Cursor, Cline, GitHub Copilot, Augment Code, Roo Code, Kilo Code, Codex, Gemini CLI
* **Discovered**: Windsurf, OpenCode, Replit, Junie, and others are inventoried by [Discovery](/cli/discovery) so you can see where they're in use across your fleet
* **MCP Servers**: Policy enforcement across any MCP server a developer connects
* **Enterprise Model Endpoints**: Vertex AI, Bedrock, and other private model endpoints for sensitive-data routing
* **Authentication**: SAML/OIDC and SCIM integration
* **MDM**: Auto-configure and deploy agent settings through your device management platform
## Enterprise Features
### Security Controls
* Terminal command, MCP, and file access policy enforcement
* Automated masking and redaction of sensitive data
* Private-model routing for sensitive requests
* Compliance-grade audit logging
### Management & Visibility
* Per-developer, team, and department posture
* Agent, plugin, and MCP inventory
* Usage insights and recommendations
* Centralized AI coding tool management
## Getting Started
### Prerequisites
* **Node.js 18+** — required for the CLI. Check with `node --version`.
* **Supported operating systems for the full Quick Start:** macOS, Windows, and Linux. The CLI and the device-discovery step (the onboarding wizard's device scan and `unbound discover`) run on all three.
* On Linux installs where Node lives in a system path (e.g. the NodeSource `apt` package), `npm install -g` writes to a root-owned directory and needs `sudo`. With a user-scoped Node manager (`nvm`, `fnm`, `volta`) or a custom `npm prefix`, no `sudo` is required.
To start using Unbound Security:
1. **Install the CLI**: `npm install -g unbound-cli` (prefix with `sudo` if `npm` requires root on your system)
2. **Login**: `unbound login`
3. **Set up your AI tool** — run the command for *your* tool (pick one). Pass the mode flag so the command stays one-shot in any shell:
```bash theme={null}
# Pick the one that matches the AI tool you use
unbound setup cursor
unbound setup claude-code --gateway # or --subscription
unbound setup codex --gateway # or --subscription
unbound setup gemini-cli
unbound setup copilot
```
Running `unbound setup claude-code` or `unbound setup codex` with no flag opens an interactive mode picker — fine in a terminal, but it silently no-ops in non-interactive shells. See [Tool Setup](/cli/tool-setup) for the full list.
4. **Monitor and govern**: Track agent activity and enforce policy from the [dashboard](https://gateway.getunbound.ai/dashboard). New workspaces are routed through a short onboarding wizard at [gateway.getunbound.ai/onboarding](https://gateway.getunbound.ai/onboarding) on first visit — complete the wizard and `/dashboard` will be available.
# Deploy AI Tools via MDM
Source: https://docs.getunbound.ai/mdm-integrations/deploy-ai-tools
MDM deployment scripts to install and configure AI coding tools across managed devices
## Overview
The scripts are designed to be executed on end-user devices through your MDM (Mobile Device Management) platform. They configure each AI coding tool to route through the Unbound Gateway.
All scripts **must be run as root** (or with administrator privileges) — each one checks for it and exits with `This script requires administrator/root privileges` otherwise.
The commands below are written with `sudo` so they work when you run them by hand. Your MDM provider already executes in a root context, so drop the `sudo` prefix when you paste them into a deployment profile.
Before deploying, ensure your [MDM provider is connected](/mdm-integrations/overview) or [devices are imported](https://gateway.getunbound.ai/settings?tab=devices) and you have an admin API key from [gateway.getunbound.ai/connect](https://gateway.getunbound.ai/connect).
In every command below, replace the following placeholders with your actual values:
* **`YOUR_ADMIN_API_KEY`** — an admin API key from [gateway.getunbound.ai/connect](https://gateway.getunbound.ai/connect)
* **`https://backend.getunbound.ai`** — if your organization uses a custom backend URL, replace this with your own
***
## Cursor
Run the following command to configure Cursor:
```bash theme={null}
sudo python3 -c "$(curl -fsSL https://getunbound.ai/setup/cursor/mdm-install)" --backend-url https://backend.getunbound.ai --api-key YOUR_ADMIN_API_KEY
```
***
## Claude Code
Claude Code supports two deployment methods depending on how your organization uses Anthropic.
### Method 1: Using API Key
Use this method to route all Claude Code traffic through the Unbound Gateway without requiring an Anthropic subscription on the device:
```bash theme={null}
sudo python3 -c "$(curl -fsSL https://getunbound.ai/setup/claude-code/gateway/mdm-install)" --backend-url https://backend.getunbound.ai --api-key YOUR_ADMIN_API_KEY
```
### Method 2: With Anthropic Account
Use this method if the user already has an active Anthropic subscription. This configures hooks for tracking and analytics while preserving the existing Anthropic authentication:
```bash theme={null}
sudo python3 -c "$(curl -fsSL https://getunbound.ai/setup/claude-code/hooks/mdm-install)" --backend-url https://backend.getunbound.ai --api-key YOUR_ADMIN_API_KEY
```
***
## Gemini CLI
Run the following command to configure Gemini CLI:
```bash theme={null}
sudo python3 -c "$(curl -fsSL https://getunbound.ai/setup/gemini-cli/gateway/mdm-install)" --backend-url https://backend.getunbound.ai --api-key YOUR_ADMIN_API_KEY
```
***
## Codex
Codex supports two deployment methods depending on how your organization uses OpenAI.
### Method 1: Using API Key
Use this method to route all Codex traffic through the Unbound Gateway without requiring an OpenAI subscription on the device:
```bash theme={null}
sudo python3 -c "$(curl -fsSL https://getunbound.ai/setup/codex/gateway/mdm-install)" --backend-url https://backend.getunbound.ai --api-key YOUR_ADMIN_API_KEY
```
### Method 2: With OpenAI Account
Use this method if the user already has an active OpenAI subscription. This configures hooks for tracking and analytics while preserving the existing OpenAI authentication:
```bash theme={null}
sudo python3 -c "$(curl -fsSL https://getunbound.ai/setup/codex/hooks/mdm-install)" --backend-url https://backend.getunbound.ai --api-key YOUR_ADMIN_API_KEY
```
***
## GitHub Copilot
Run the following command to configure GitHub Copilot. Hooks-based — works with both the VS Code extension and the `copilot` CLI on the user's existing Copilot subscription:
```bash theme={null}
sudo python3 -c "$(curl -fsSL https://getunbound.ai/setup/copilot/hooks/mdm-install)" --backend-url https://backend.getunbound.ai --api-key YOUR_ADMIN_API_KEY
```
***
## Augment Code
Run the following command to configure Augment Code. Hooks-based — works with both the Augment VS Code extension and the Auggie CLI:
```bash theme={null}
sudo python3 -c "$(curl -fsSL https://getunbound.ai/setup/augment/hooks/mdm-install)" --backend-url https://backend.getunbound.ai --api-key YOUR_ADMIN_API_KEY
```
***
## Remove Unbound Configuration
To remove the Unbound Gateway configuration from a device, run that tool's `mdm-install` command with only the `--clear` flag. Clearing does **not** require an API key or backend URL. This removes all Unbound-related settings for that tool and restores it to its default state.
For example, to clear Cursor:
```bash theme={null}
sudo python3 -c "$(curl -fsSL https://getunbound.ai/setup/cursor/mdm-install)" --clear
```
***
## Keep enforcement in place
Deploying through MDM installs Unbound as managed settings that standard users can't remove. Schedule the deployment to run daily so enforcement stays resilient.
Best practices for keeping Unbound active on every device with managed settings.
# Microsoft Intune
Source: https://docs.getunbound.ai/mdm-integrations/intune
Deploy Unbound to Windows devices with Intune Remediations
## Overview
Run the Unbound installer on a schedule with Intune **Remediations**.
Remediations requires Windows Enterprise E3 or E5, included in Microsoft 365 E3, E5 and F3. Without it, use the [scheduled task](#no-remediations-licence) below.
Deploy Python 3 as a Win32 app with `InstallAllUsers=1 PrependPath=1 Include_launcher=1`. A per-user install will not be found.
Allow HTTPS to `getunbound.ai`, `backend.getunbound.ai`, `api.getunbound.ai`, `raw.githubusercontent.com` and `github.com`.
***
## 1. Get your API key
Open [Configure](https://gateway.getunbound.ai/configure) and copy the key from the **API Access** panel.
***
## 2. Create the detection script
Save this as `Detect_Unbound.ps1`:
```powershell theme={null}
$base = 'C:\Program Files\Unbound'
$marker = Join-Path $base 'last-success.txt'
$owned = (Test-Path $marker) -and
((Get-Acl $base).GetOwner([Security.Principal.SecurityIdentifier]).Value -in @('S-1-5-18', 'S-1-5-32-544'))
if ($owned -and ((Get-Date) - (Get-Item $marker).LastWriteTime).TotalHours -lt 20) {
Write-Output "Unbound: ran $((Get-Item $marker).LastWriteTime)"; exit 0
}
Write-Output "Unbound: needs a run"; exit 1
```
***
## 3. Create the remediation script
Save this as `Remediate_Unbound.ps1`, replacing `YOUR_ADMIN_API_KEY` with your key:
```powershell theme={null}
$base = 'C:\Program Files\Unbound'
New-Item -ItemType Directory -Force -Path $base | Out-Null
icacls $base /setowner '*S-1-5-32-544' /T /C | Out-Null
icacls $base /reset /T /C | Out-Null
icacls $base /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' | Out-Null
$acl = Get-Acl $base
$allowed = @('S-1-5-18', 'S-1-5-32-544')
if ($acl.GetOwner([Security.Principal.SecurityIdentifier]).Value -notin $allowed -or
($acl.Access | Where-Object {
$_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value -notin $allowed })) {
throw "Could not secure $base. Refusing to continue."
}
Remove-Item (Join-Path $base '*') -Force -Recurse -ErrorAction SilentlyContinue
$installer = Join-Path $base 'onboard.ps1'
Invoke-WebRequest -Uri 'https://getunbound.ai/setup/mdm/windows/onboard' -OutFile $installer -UseBasicParsing -ErrorAction Stop
& $installer -ApiKey 'YOUR_ADMIN_API_KEY' -Backfill
$code = $LASTEXITCODE
if ($code -eq 0) {
try { Set-Content -Path (Join-Path $base 'last-success.txt') -Value (Get-Date -Format 'o') -ErrorAction Stop }
catch { Write-Output "Installed, but could not write the marker. $_"; $code = 1 }
}
Remove-Item $installer -Force -ErrorAction SilentlyContinue
exit $code
```
The folder sits under `C:\Program Files`, which standard users cannot write to, and is locked to SYSTEM and administrators. Nothing can be planted there ahead of the script.
`-Backfill` imports past GitHub Copilot, Claude Code and Codex sessions. Re-running never duplicates a session.
***
## 4. Create the package in Intune
Go to **Devices → Manage devices → Scripts and remediations → Remediations → Create script package**.
Name the package `Unbound daily onboard` and upload both files as UTF-8 without a BOM.
**Run using logged-on credentials: No**. **Enforce signature check: No**. **Run in 64-bit PowerShell: Yes**.
Assign to your developer device group and set the schedule to **Daily**.
To run it on one device now, open that device in Intune and choose **Run remediation**.
***
## Verify
Device status shows **Without issues**.
`C:\Program Files\Unbound\last-success.txt` updates daily.
Devices appear with a recent last-seen time.
***
## Troubleshooting
| Symptom | Fix |
| -------------------------------------------- | ----------------------------------------------------------------------------- |
| `Python 3 is required but not found in PATH` | Re-deploy Python with `InstallAllUsers=1 PrependPath=1`, then reboot. |
| Remediation shows **Failed** | Open the device in Intune and read the output. It names the step that failed. |
| Copilot rows show `auto` | Allow one daily cycle. Sessions VS Code has already rotated cannot be read. |
***
## No Remediations licence
Deploy this once as a Platform script, run as SYSTEM in 64-bit. The task script holds your API key, so it lives under `C:\Program Files`, which standard users cannot write to, locked to SYSTEM and administrators.
```powershell theme={null}
$base = 'C:\Program Files\Unbound'
New-Item -ItemType Directory -Force -Path $base | Out-Null
icacls $base /setowner '*S-1-5-32-544' /T /C | Out-Null
icacls $base /reset /T /C | Out-Null
icacls $base /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' | Out-Null
$acl = Get-Acl $base
$allowed = @('S-1-5-18', 'S-1-5-32-544')
if ($acl.GetOwner([Security.Principal.SecurityIdentifier]).Value -notin $allowed -or
($acl.Access | Where-Object {
$_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value -notin $allowed })) {
throw "Could not secure $base. Refusing to write the API key."
}
Remove-Item (Join-Path $base '*') -Force -Recurse -ErrorAction SilentlyContinue
$runner = Join-Path $base 'run-unbound.ps1'
@'
$installer = 'C:\Program Files\Unbound\onboard.ps1'
Invoke-WebRequest -Uri 'https://getunbound.ai/setup/mdm/windows/onboard' -OutFile $installer -UseBasicParsing -ErrorAction Stop
& $installer -ApiKey 'YOUR_ADMIN_API_KEY' -Backfill
$code = $LASTEXITCODE
if ($code -eq 0) {
try { Set-Content -Path 'C:\Program Files\Unbound\last-success.txt' -Value (Get-Date -Format 'o') -ErrorAction Stop }
catch { Write-Output "Installed, but could not write the marker. $_"; $code = 1 }
}
Remove-Item $installer -Force -ErrorAction SilentlyContinue
exit $code
'@ | Set-Content -Path $runner -Encoding UTF8
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$runner`""
$trigger = New-ScheduledTaskTrigger -Daily -At 9:00am
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 2)
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName 'Unbound Daily Onboard' -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Force -ErrorAction Stop | Out-Null
Start-ScheduledTask -TaskName 'Unbound Daily Onboard' -ErrorAction Stop
```
Remove it with `Unregister-ScheduledTask -TaskName 'Unbound Daily Onboard' -Confirm:$false`.
***
## Remove Unbound
Run the installer with `-Clear` in an elevated PowerShell, then delete `C:\Program Files\Unbound`.
Best practices for keeping Unbound active on every device with managed settings.
# Jamf Binary Runtime
Source: https://docs.getunbound.ai/mdm-integrations/jamf/binary
Deploy Unbound to Macs with a signed package
## Overview
The binary runtime installs a signed, self-contained package on macOS.
**Requirements:** macOS 13 or later, 300 MB free disk space, and HTTPS to `getunbound.ai`, `backend.getunbound.ai`, `api.getunbound.ai` and `unbound-release-artifacts.s3.us-west-2.amazonaws.com`.
***
## 1. Create the script
Go to **Settings → Computer Management → Scripts → New**. Name it `Unbound daily onboard` and paste this into the **Script** tab:
```bash theme={null}
#!/bin/bash
API_KEY="$4"
[ -n "$API_KEY" ] || { echo "Parameter 4 (API key) is empty."; exit 1; }
INSTALLER=$(mktemp)
trap 'rm -f "$INSTALLER"' EXIT
curl -fsSL --max-time 120 -o "$INSTALLER" https://getunbound.ai/setup/mdm/onboard.sh \
|| { echo "Could not download the installer."; exit 1; }
bash "$INSTALLER" --api-key "$API_KEY" \
--backend-url https://backend.getunbound.ai \
--gateway-url https://api.getunbound.ai \
--backfill
CODE=$?
MARKER="/Library/Application Support/Unbound/last-success.txt"
if [ $CODE -eq 0 ]; then
mkdir -p "$(dirname "$MARKER")" && date -u +%Y-%m-%dT%H:%M:%SZ > "$MARKER" \
|| { echo "Installed, but could not write $MARKER."; exit 1; }
fi
exit $CODE
```
On the **Options** tab, label **Parameter 4** as `API key`.
`--backfill` imports past Claude Code and Codex sessions. Re-running never duplicates a session.
***
## 2. Create the policy
Go to **Computers → Policies → New**.
Name it `Unbound daily onboard`. Set Trigger to **Recurring Check-in** and Execution Frequency to **Once every day**.
Add the script and paste your API key into **Parameter 4**.
Select your developer Mac group, then save.
***
## Troubleshooting
| Symptom | Fix |
| ---------------------------- | -------------------------------------------------------------------------------- |
| `macOS < required 13` | Use the [Python installer](/mdm-integrations/jamf/python) on older Macs. |
| `need 300MB free` | Free up disk space and run the policy again. |
| `artifact host unreachable` | Allow `unbound-release-artifacts.s3.us-west-2.amazonaws.com` through your proxy. |
| Checksum or Team ID mismatch | A proxy modified the package in transit. Fix the proxy. |
***
## Remove Unbound
```bash theme={null}
sudo bash -c "$(curl -fsSL https://getunbound.ai/setup/mdm/onboard.sh)" -- --clear
```
Then delete `/Library/Application Support/Unbound`.
# Jamf
Source: https://docs.getunbound.ai/mdm-integrations/jamf/overview
Deploy Unbound to Macs with a Jamf script and policy
## Overview
Deploy Unbound with a Jamf **Script** and a **Policy** set to run daily. Choose one installer.
| | Binary runtime | Python installer |
| --------------------- | ------------------------------------- | ---------------- |
| Python on each device | Not needed | Required |
| macOS version | 13 or later | Any |
| Package integrity | SHA-256 pinned, Developer ID verified | — |
Recommended for Mac fleets.
Use on Macs older than macOS 13.
***
## Get your API key
Open [Configure](https://gateway.getunbound.ai/configure) and copy the key from the **API Access** panel. Both installers take it as Jamf **Parameter 4**.
Jamf reserves `$1` to `$3`. Custom parameters start at Parameter 4.
***
## Verify
The policy shows **Completed** in **Policies → Logs**.
`/Library/Application Support/Unbound/last-success.txt` updates daily.
Devices appear with a recent last-seen time.
To run a policy on one Mac now:
```bash theme={null}
sudo jamf policy -id POLICY_ID
```
The Mac must be in scope. To run it again the same day, flush the policy log under **Policies → your policy → Logs → Flush**.
***
## Report status in Jamf
Add an Extension Attribute to see the last successful run in inventory. Works with either installer.
Go to **Settings → Computer Management → Extension Attributes → New**. Set Data Type to **String** and Input Type to **Script**:
```bash theme={null}
#!/bin/bash
M="/Library/Application Support/Unbound/last-success.txt"
if [ -f "$M" ]; then echo "$(cat "$M")"; else echo "never"; fi
```
Best practices for keeping Unbound active on every device with managed settings.
# Jamf Python Installer
Source: https://docs.getunbound.ai/mdm-integrations/jamf/python
Deploy Unbound to Macs that already have Python 3
## Overview
Works on any Mac with Python 3, including Macs older than macOS 13.
On macOS, `/usr/bin/python3` does not work until the Xcode Command Line Tools are installed. Deploy the Command Line Tools or the [python.org](https://python.org) package. Or use the [binary runtime](/mdm-integrations/jamf/binary), which is self-contained.
Allow HTTPS to `getunbound.ai`, `backend.getunbound.ai`, `api.getunbound.ai`, `raw.githubusercontent.com` and `github.com`.
***
## 1. Create the script
Go to **Settings → Computer Management → Scripts → New**. Name it `Unbound daily onboard` and paste this into the **Script** tab:
```bash theme={null}
#!/bin/bash
API_KEY="$4"
[ -n "$API_KEY" ] || { echo "Parameter 4 (API key) is empty."; exit 1; }
for P in /usr/bin/python3 /Library/Frameworks/Python.framework/Versions/Current/bin/python3; do
"$P" -c '' 2>/dev/null && PY="$P" && break
done
[ -n "$PY" ] || { echo "No usable python3. Deploy the Xcode Command Line Tools or the python.org package."; exit 1; }
INSTALLER=$(mktemp)
trap 'rm -f "$INSTALLER"' EXIT
curl -fsSL --max-time 120 -o "$INSTALLER" https://getunbound.ai/setup/mdm/onboard \
|| { echo "Could not download the installer."; exit 1; }
"$PY" "$INSTALLER" --api-key "$API_KEY" \
--gateway-url https://api.getunbound.ai \
--frontend-url https://gateway.getunbound.ai \
--backend-url https://backend.getunbound.ai \
--backfill
CODE=$?
MARKER="/Library/Application Support/Unbound/last-success.txt"
if [ $CODE -eq 0 ]; then
mkdir -p "$(dirname "$MARKER")" && date -u +%Y-%m-%dT%H:%M:%SZ > "$MARKER" \
|| { echo "Installed, but could not write $MARKER."; exit 1; }
fi
exit $CODE
```
On the **Options** tab, label **Parameter 4** as `API key`.
The script only uses root-owned interpreters. A Homebrew or `/usr/local` python can be replaced by the logged-in user, and this script runs as root.
`--backfill` imports past Claude Code and Codex sessions. Re-running never duplicates a session.
***
## 2. Create the policy
Go to **Computers → Policies → New**.
Name it `Unbound daily onboard`. Set Trigger to **Recurring Check-in** and Execution Frequency to **Once every day**.
Add the script and paste your API key into **Parameter 4**.
Select your developer Mac group, then save.
***
## Troubleshooting
| Symptom | Fix |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `No usable python3` | Install the Xcode Command Line Tools or the [python.org](https://python.org) package, then run the policy again. |
| `Could not download the installer` | Egress to `getunbound.ai` is blocked. Check your allow-list, including any TLS-inspecting proxy. |
| Policy shows **Failed** | Open **Policies → Logs** and read the output. It names the step that failed. |
***
## Remove Unbound
```bash theme={null}
sudo python3 -c "$(curl -fsSL https://getunbound.ai/setup/mdm/onboard)" --clear
```
Then delete `/Library/Application Support/Unbound`.
# Connect MDM Provider
Source: https://docs.getunbound.ai/mdm-integrations/overview
Streamline AI tool deployments across your organization
## Why MDM Integrations?
MDM (Mobile Device Management) integrations enable you to seamlessly deploy and configure AI coding tools across your organization's devices. By connecting your MDM provider to Unbound, you can:
* **Automate tool deployments**: Push AI coding tools to users automatically
* **Identify device owners**: Map devices to users for better visibility and management
* **Streamline onboarding**: New team members get AI tools configured without manual setup
**Settings URL:** [https://gateway.getunbound.ai/settings?tab=integrations](https://gateway.getunbound.ai/settings?tab=integrations)
Click here to manage your MDM integrations
## How to Connect
1. Go to [Settings > Integrations](https://gateway.getunbound.ai/settings?tab=integrations)
2. Click **Connect** on the MDM provider you want to integrate
3. Enter your API key
4. Click **Add**
Once connected, you'll see a **Connected** status badge on the integration card.
Keep your API key secure. It provides access to your MDM organization data.
## What Happens After Integration?
Once your MDM is connected, Unbound can:
* **Identify users by device**: Look up device owners using device serial numbers
* **Auto-configure tools**: Deploy AI tools with pre-configured Unbound gateway settings
* **Track deployments**: Monitor which users have which AI tools installed
## Supported AI Tools
MDM integrations help deploy and configure these AI coding tools with Unbound:
* **Cursor** - AI-powered code editor
* **Claude Code** - Anthropic's agentic coding tool
* **Gemini CLI** - Google's Gemini in the terminal
* **Codex** - OpenAI's coding assistant
* **GitHub Copilot** - GitHub's AI pair programmer (VS Code extension and CLI)
* **Augment Code** - AI coding assistant for large codebases (VS Code extension and Auggie CLI)
* **AI Tools Discovery** - Discover and manage AI tools across your organization
# Tamper Resistance
Source: https://docs.getunbound.ai/mdm-integrations/tamper-resistance
Keep Unbound enforcement active on every device using managed settings
When you deploy Unbound through your MDM, enforcement is installed as **managed settings** — administrator-level configuration that standard users can't change or remove. This keeps Unbound active on every device, even if a user tries to turn it off, and it works the same way on macOS, Windows and Linux.
This page covers why managed settings are tamper-resistant, how to keep them in place with a daily schedule, and how to spot devices where enforcement has drifted.
## Why managed settings are tamper-resistant
Deploying through MDM is different from a per-user install. Instead of writing configuration into each user's personal settings — which that user can edit — Unbound is installed into the device's **managed settings**, owned by the administrator.
Managed settings live in an administrator-owned, protected location. A standard (non-admin) user can't modify or delete them.
Managed settings override personal settings, so a user can't disable enforcement by changing their own configuration.
Managed deployment must be run with administrator privileges. See [Deploy AI Tools via MDM](/mdm-integrations/deploy-ai-tools) for the per-tool install commands.
## Reapply on a daily schedule
Schedule the MDM deployment to run **once a day** on each device. A daily run keeps enforcement resilient: if a setting is ever changed or removed, the next run restores it automatically — so any gap closes within 24 hours without manual intervention.
Push the [MDM install command](/mdm-integrations/deploy-ai-tools) for each AI tool to your managed devices.
Configure your MDM platform to re-run the same command on a daily schedule. Re-running is safe — it reapplies the managed settings and changes nothing on devices that are already correct.
Review the [Devices page](https://gateway.getunbound.ai/settings?tab=devices) to confirm enforcement is active across your fleet.
## Keep an eye on your fleet
The [Devices page](https://gateway.getunbound.ai/settings?tab=devices) gives you a fleet-wide view of where Unbound is active and when each device last reported in. Use it to confirm coverage: a device that hasn't reported recently, or that an administrator knows was reinstalled, is worth a closer look.
When a device needs attention, an administrator can re-run the MDM deployment on it at any time to restore enforcement immediately, and the daily schedule does this automatically.
Pair the daily schedule with a regular Devices-page review. The schedule heals drift automatically; the review tells you *which* devices keep needing it, so you can follow up with those users.
## macOS, Windows and Linux
Managed settings are administrator-protected on every platform, so the guarantee on the enforcement wiring is the same:
| Platform | Who can change enforcement |
| ----------- | -------------------------------------------------------------------------------- |
| **macOS** | Only an administrator. Standard users can't edit or remove the managed settings. |
| **Windows** | Only an administrator. Standard users can't edit or remove the managed settings. |
| **Linux** | Only root. Managed settings live in a root-owned system directory. |
One difference is worth knowing. Windows stores Unbound's environment values machine-wide, so changing them needs administrator rights. macOS and Linux have no machine-wide equivalent, so the same values are written into each user's shell profile alongside a per-user config file. A standard user can edit those and stop their own device reporting — the enforcement wiring above stays administrator-owned either way, and the daily re-run restores the values within 24 hours. It is the strongest reason to keep the schedule daily rather than weekly.
If users on a device have local administrator rights, they can change administrator-level settings. To keep enforcement fully tamper-resistant, limit local admin rights and rely on the daily schedule and Devices-page review to catch any changes.
## Windows Subsystem for Linux (WSL)
Many developers run AI coding tools like Claude Code **inside WSL** (Windows Subsystem for Linux) rather than directly on Windows. WSL needs special attention, because it can sidestep the tamper-resistance guarantees above if it isn't configured correctly.
Inside a WSL distribution, a standard Windows user can become **root with no Windows administrator rights** — for example with `sudo` (most distributions set up a sudo-capable user on first launch), or by starting WSL as root from a Windows terminal (`wsl -u root`). Neither triggers a Windows UAC prompt. So the Linux-side managed settings file isn't administrator-protected the way it is on native Windows or macOS — a WSL user can edit or remove it and disable enforcement.
### Close the gap with `wslInheritsWindowsSettings`
Set **`wslInheritsWindowsSettings: true`** in your **Windows** managed settings. Claude Code running inside WSL then also reads the Windows-side managed settings, with the Windows settings taking precedence. Because those Windows settings require administrator rights to change, a WSL user can no longer override enforcement.
```json theme={null}
{
"wslInheritsWindowsSettings": true
}
```
`wslInheritsWindowsSettings` is honored **only from OS-level Windows managed settings** — delivered through Group Policy (GPO) or Intune to `HKLM\SOFTWARE\Policies\ClaudeCode`, or the managed settings file under `C:\Program Files\ClaudeCode`. It is **not** applied from server-managed (cloud) settings, so WSL hardening depends on the MDM-delivered endpoint settings.
If you currently deploy Unbound only through server-managed (cloud) settings, add an OS-level Windows managed-settings layer — via GPO or Intune to `HKLM\SOFTWARE\Policies\ClaudeCode`, or the `C:\Program Files\ClaudeCode` file — on any device that can run WSL. Reach out to [support@unboundsecurity.ai](mailto:support@unboundsecurity.ai) if you'd like help setting up the Windows policy layer.
### Best practices for WSL
Set `wslInheritsWindowsSettings: true` in your Windows managed settings whenever anyone on the fleet uses WSL.
For managed fleets, install Claude Code natively on Windows (administrator-protected) rather than via npm inside WSL where you can.
Push the Windows managed settings through Group Policy or Intune so the OS reapplies them on its own schedule.
If WSL is required, pair the flag with a regular Devices-page review to catch any device that stops reporting.
## Best practices
Use managed deployment rather than per-user setup so enforcement is administrator-owned from the start.
A daily schedule restores any removed or changed settings within 24 hours.
Standard users can't touch managed settings — keeping admin rights scarce keeps enforcement intact.
Check in regularly to confirm every device is still reporting and enforcement is active.
Questions? Reach us in Slack or email [support@unboundsecurity.ai](mailto:support@unboundsecurity.ai).
## Seeing tampered setups in the dashboard
When a device's Unbound setup is removed or altered, it surfaces in three places:
* **Home** — a **Tampered Devices** recommendation counting the affected machines, captioned *"Unbound setup was changed or removed"*. Click it to open the user list filtered to those people.
* **Users** — arriving from that card applies an **Unbound Setup: Removed** filter, which stays in the URL so the view can be shared.
* **The user side panel** — an **Unbound Setup** section headed **Setup Tampered**, listing each affected machine by serial number and, beneath it, one line per tool: *Setup removed* where the managed configuration is gone entirely, *Setup modified* where only the hook script was lost.
Developers can repair their own machines with [`unbound doctor --fix`](/cli/doctor).
Read this as **"needs attention now"**, not as an audit trail. Specifically:
* It covers **MDM-deployed installs only**. A per-user install that a developer deletes is never counted here.
* It is **retrospective** — nothing is detected until the setup command next runs on that device, so there is up to a full schedule interval of lag.
* **Offline is not tampered.** A device that is switched off, off the network, or whose scheduled run has stopped firing reports nothing and will not appear. Coverage gaps belong to the Devices view instead.
* A **first-ever install** is never flagged.
* It **clears itself** — a later clean run drops the device off the list.
* **No timestamp of the tampering is shown**, deliberately: only the date of the discovery run is knowable.
* There is **no webhook and no notification**.
This is different from the **Tampered** status [`unbound doctor`](/cli/doctor) reports. That is one machine's local verdict, run by the developer on demand. This is your fleet-wide view, built from what devices report back.
# Cowork Starting Policies
Source: https://docs.getunbound.ai/playbook/cowork-starting-policies
The day-one policy pack for Claude Cowork — protect the data in prompts and draw the line on file, shell, and connector actions for general knowledge work
Cowork isn't a coding agent. It's a general-purpose desktop agent for knowledge work — research, analysis, operations, finance, legal — that moves across your local files, the apps you use, the browser, and your connected MCP tools to finish multi-step tasks. Its risk surface is different: less "tear down production," more "read the wrong file, paste a customer's card number into a prompt, or upload an export to the open web."
**This is the pack that draws that line for Cowork.** It covers three layers — the data in your prompts, the files and shell actions on the machine, and the connectors Cowork can reach — using the exact values Unbound matches on.
## Why Cowork needs a guardrail layer
Cowork is an autonomous agent, not a chatbot. It works across your local files, the browser, and connected MCP tools, and it acts **under the user's identity, on content it didn't write.** That is what makes it useful, and what makes it worth governing:
* **It reads untrusted content and acts on it.** An instruction hidden in a document, an email, or a web page can turn into a command the agent runs. Prompt injection is a known, unsolved class of attack, and a per-action approval prompt is a limited backstop once a user is clicking through a lot of them.
* **Defense in depth.** The controls that ship inside the tool are one layer. An independent layer at the endpoint — one that sees the actual tool calls and file reads — gives you a second.
* **A record you control.** For compliance and incident review, security teams want an independent, exportable trail of what the agent did, held outside the tool itself.
Unbound sits at that endpoint layer. Every Cowork session streams into your **Logs** and **Analytics** — an independent record you own — and the policies in this pack let you see and govern the specific actions that carry the most risk.
## What you're defending against
The pack below maps to the threat vectors that matter most for an autonomous desktop agent. Each row points to the policies that cover it.
| Threat | What it looks like | Where this pack covers it |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Credential & token theft | The agent reads a secrets file or inherits a live session and lifts keys | Credential & key files and env-variable dumps |
| Data exfiltration | A task quietly ships private files out through a channel the agent already trusts | Data transfer, browser automation, and the storage / email / messaging connectors |
| Excessive agency | A high-impact action — send, write, delete, run — runs before anyone reviews it | Bulk deletion, config overwrite, package install, and the MCP write / destructive rules |
| The "lethal trifecta" | Private-data access **plus** untrusted content **plus** an external channel — the recipe for command-driven theft | The prompt guardrails and the outbound-connector rules, together |
| MCP tool poisoning & malicious skills | A compromised or unverified connector steers the agent silently on every call | The sanctioned-MCP allowlist plus the per-connector MCP rules |
| Indirect prompt injection | Instructions hidden in a file, email, or page run as the user's own commands | Why the tool-layer rules matter — they catch the *action* even when the instruction slips through |
| Audit gap | Without an independent record, you can't show an auditor what the agent did on a machine at a given time | Every match lands in **Logs** and **Analytics**, attributed to user and session |
| Shadow AI | Cowork run on a personal account moves data outside org controls | Discovery, plus the sanctioned-MCP allowlist |
Create these under **Policies → Security Policies** and **Policies → Tool Policies**. Each row below is a **match to create** — the action is yours to set based on the traffic you see. Leave **User Groups** empty to apply org-wide, or scope to a team.
Building for AI **coding** agents (Claude Code, Cursor, Codex, …) instead? See [Recommended Starting Policies](/playbook/recommended-starting-policies) — the terminal-command pack for engineering work.
## Live in three steps
Add the rows that match how your teams use Cowork — the data in prompts, the files and shell on the machine, and the connectors it reaches.
Paste the **Try it** prompt into a Cowork session and watch the match land in **Logs** and **Analytics**, attributed to the user and session.
After a few days of activity, tighten the rules that matter and relax anything noisy. Your controls end up shaped by how your team actually works, not generic defaults.
The **Try it** column in every table gives a natural-language prompt you can paste into a Cowork session to see the policy match end-to-end — the arrow shows the detection or action Unbound catches.
## Protect the data in prompts
The fastest way sensitive data leaves your org through an AI agent isn't a command — it's a prompt. Cowork users paste configs, credentials, and customer details straight into the prompt for it to summarize, clean, or draft from. Two guardrails catch the highest-value data before it reaches the model.
| Policy | Why it matters | Guardrail · Match (**If**) | Try it (prompt → what Unbound catches) |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Secrets in prompts | Analysts paste configs, connection strings, and env snippets for Cowork to clean up or analyze — shipping live credentials straight to the model | **Secrets** · API keys, database connection strings, cryptographic keys | *"Help me tidy this config: `AWS_ACCESS_KEY_ID=AKIAVQ3EYIY4LIRVHK37` `AWS_SECRET_ACCESS_KEY=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`"* → Unbound flags the AWS access key |
| Payment-card data in prompts | Revenue, billing, and finance work routinely touches cardholder data; a pasted card number is PCI-scope data leaving your boundary | **PII** · Credit Card Number | *"Draft a renewal note for this account — card on file 4111 1111 1111 1111."* → Unbound flags the credit-card number |
## Files and shell on the machine
Cowork's headline skill is working directly on your machine — reading, organizing, rewriting, and deleting files, and (for roles that allow it) running shell commands. These rules cover the actions where that goes wrong: reading secrets, dumping the environment, overwriting or deleting at scale, uploading data off the box, and pulling in new software. The family and field values are exactly what Unbound matches on.
| Policy | Why it matters | Command Family · Match (**If**) | Try it (prompt → action Unbound catches) |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Reading credential & key files | Cowork ranges across local files to synthesize and organize — including the dotfiles that hold your cloud keys, SSH keys, and tokens | **Read File** · Path `.ssh/`, `.aws/credentials`, `.env`, `.pem` | *"Read \~/.aws/credentials and tell me which profiles I have."* → `cat ~/.aws/credentials` |
| Environment-variable dumps | "Show me my environment" is a routine setup step, but env dumps are exactly where tokens and keys live | **Environment Exposure** · Method `env`, `printenv` | *"Print all my environment variables so we can see what's configured."* → `env` |
| Writing to system or config paths | Cowork edits and regenerates files in place; an overwrite or append to a system or shared config path is silent and hard to undo | **Write File** · Path `/etc/`, `/usr/`, `.config/` | *"Add a hosts entry pointing api.internal to 10.0.0.5."* → `echo "10.0.0.5 api.internal" >> /etc/hosts` |
| Bulk file deletion | Cowork's signature file skill — rename, sort, dedupe — deletes and overwrites at scale; one bad pattern loses real work | **Delete File** · Path `ANY` *(logs every delete; tighten to a directory once you've seen the traffic)* | *"Clean up my Downloads folder — delete anything older than a year."* → `rm -rf ~/Downloads/old` |
| Data transfer to external endpoints | A legit export and an exfiltration look identical, and a team's customer lists and data exports are the crown jewels | **Data Transfer** · `ANY` *(logs every outbound transfer; scope to a destination or protocol once you've seen the traffic)* | *"Upload accounts.csv to [https://filebin.example.com](https://filebin.example.com) so I can share it."* → `curl -F "file=@accounts.csv" …` |
| Installing software packages | An agent that installs packages pulls unreviewed code and supply-chain risk onto the machine | **Package Management** · Operation `install` | *"Install the AWS CLI so we can pull the exports."* → `brew install awscli` |
## Connectors and the browser
Cowork reaches out through MCP connectors and a browser, and that's where a task quietly becomes an exfiltration path — a query that dumps a table, a message that broadcasts customer data, an upload into a web form. MCP policies target a **canonical group** (a logical service, matched across whatever server name your users configured), then either specific **tools** or a tool **action type** (read / write / destructive).
| Policy | Why it matters | MCP Group · Match (**If**) | Try it (prompt → tool Unbound catches) |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Browser automation | Cowork's browser connector can type into external web forms and open arbitrary sites — the exfiltration and malicious-site path for a desktop agent | **Playwright** · tools `browser_navigate`, `browser_type`, `browser_file_upload` | *"Open filebin.example.com and upload the accounts export."* → `browser_file_upload` |
| Data-warehouse queries | One query can pull an entire table of customer records — the largest-blast-radius data pull an analyst agent can make | **Snowflake** · action type `read` (or the query / run tools) | *"Run `SELECT * FROM customers` and export the results."* → `run_query` |
| Posting to messaging | An agent that can post to channels can broadcast customer or internal data to a wide — or externally-shared — audience | **Slack** · action type `write` (or the `send_message` tool) | *"Post the Q3 pipeline numbers to #general."* → `send_message` |
| Outbound email | Sending email is how data leaves the building; an agent sending on your behalf is high-impact | **Google Workspace** · action type `write` (or the Gmail `send_email` tool) | *"Email this account summary to [partner@external.com](mailto:partner@external.com)."* → `send_email` |
| Cloud document store | Copying or exporting a whole folder of contracts, comp sheets, or board docs is silent bulk data-exfil | **Google Drive** · action type `write` (or the `copy_file` / `create_file` tools) | *"Copy the whole 'Board Docs' folder into my personal Drive."* → `copy_file` |
| Cloud file storage | A "share with anyone" link turns your document store into an exfiltration channel | **Box** · action type `write` (or the `create_shared_link` tool) | *"Share the Customers folder with a public link."* → `create_shared_link` |
| Code-repository writes | An agent that can write to repos can push code, open pull requests, or delete branches — and repos hold secrets and IP | **GitHub** · action type `write` / `destructive` (or `create_pull_request`, `delete_*`) | *"Commit these changes and open a PR to main."* → `create_pull_request` |
| Payments | Refunds and payment links move real money — the clearest financial-action risk for a finance or ops agent | **Stripe** · action type `write` (or the `create_refund` tool) | *"Refund the last 20 charges from Acme."* → `create_refund` |
**One control covers every connector: the sanctioned-MCP allowlist.** New MCP servers appear constantly and many are unverified. Set the allowlist so only approved servers are reachable and any unknown or unofficial one is caught — the single highest-leverage MCP control, and a clean complement to the per-tool rules above.
## Go live
Create the rows that fit how your teams use Cowork and test each one in seconds with the prompt in the last column — every match lands in **Logs** and **Analytics**, attributed to the user and session. From there the action is yours: keep a rule on visibility, or turn it into an enforced boundary once you've seen the traffic. Your controls end up shaped by how your team actually works, not generic defaults.
# Onboarding Playbook
Source: https://docs.getunbound.ai/playbook/introduction
A guided tour of the platform — what each part does, and how to get value in your first hour
**Playbook:** **Onboarding** · [Recommended Starting Policies](/playbook/recommended-starting-policies) · [Threat Model](/playbook/threat-model) · [Tool Policy Examples](/playbook/policy-examples)
Your AI coding tools — Claude Code, Cursor, Codex, Copilot, and more — now route through Unbound. We see every prompt, every terminal command, and every MCP tool call your agents make, and we enforce your policies **inline, before anything runs**. Nothing changes for your users. Everything changes for your security team.
This playbook walks you through what's in the platform, what each part is for, and how to get value fast.
## 1. Getting started (5 minutes)
Go to your Unbound gateway at [gateway.getunbound.ai](https://gateway.getunbound.ai) — or your organization's custom tenant domain, if you have one.
Open **Connect → AI Coding Tools**, pick your tool, and follow the one-line setup. For Claude Code and Codex, choose **Subscription** mode — keep your existing Claude / OpenAI subscription while Unbound observes and enforces via hooks.
Open **Connect → Device Deployment**, choose the tool and platform (macOS / Windows), and copy the single MDM install command. It deploys Unbound to every user on a device with no per-user setup.
Once a tool is connected, its activity starts flowing into Unbound immediately.
## 2. The dashboard — your home base
Your landing page is an at-a-glance health view of your whole org: devices and tools connected, agentic activity (terminal commands and MCP calls), how many actions were blocked / warned / allowed, total spend, and recommendations. It's built to be skimmed — start here each day, then click into whatever needs attention.
## 3. See what your AI tools are actually doing
Before you write a single policy, get visibility. Open **AI Tools Discovery → Summary**. This is your mission control: it inventories every AI tool detected across your org and flags risk.
**Three things to check on day one:**
Unsanctioned AI tools users installed on their own. Review the list and decide what's approved.
Users running with auto-accept *and* no deny rules *and* sandboxing off — the highest-risk setups. The **Permissions** sub-page shows exactly who.
MCP servers whose publisher is *unofficial* or *unknown*. The **MCP Servers** sub-page flags each one so you can spot shadow MCP.
The other sub-pages — **Users, Tools, Tool Rules, Skills, Setup** — let you drill into per-user detail. You don't need them on day one.
## 4. Tool Policies — guardrails on what AI can do
Tool Policies govern the **actions** AI agents take — the terminal commands they run and the MCP tools they call — and stop the dangerous ones before they execute. Find them under **Policies → Tool Policies**.
### What each action does
| Action | Stops the operation? | Logged? | What your user sees |
| -------------------------- | -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Audit** | No | Yes | Nothing — it runs normally and you get a log entry |
| **Warn** | No | Yes | A warning, then it proceeds |
| **Block** | Yes | Yes | An error explaining the block |
| **Require Slack Approval** | Pauses | Yes | A Slack DM to approve or deny; the agent retries after the decision (needs the [Slack integration](/integrations/slack)) |
**Warn** shows an inline confirmation prompt on **Claude Code**, **Copilot**, **Codex**, **Augment Code**, and **Claude Cowork**.
Two tools behave differently, and it matters:
* On **Cursor**, a Warn policy is **not enforced** — the command runs, with no prompt. Use **Block** on Cursor if you need the action stopped.
* On **OpenClaw**, Warn **blocks** the command outright, because OpenClaw has no confirmation prompt to show.
If your users run agentic workflows, start with **Audit** to learn what's normal, then tighten to **Warn** / **Block**. A hard **Block** mid-chain returns an error that can interrupt a multi-step agent task.
### Three ways to create a policy
1. **Guided form (UI).** Open **Policies → Tool Policies** and click **Create Policy**, then choose **Terminal Commands** or **MCP Actions**. Build the rule with dropdowns: **When** (command family) → **If** (field to match + pattern) → **Then** (action) → optionally scope to **User Groups**. A live preview shows the rule in plain English as you build it.
2. **Describe it in plain English.** The create dialog has a *"Describe a policy or paste a command…"* box. Type what you want — e.g. *"Block any database command that drops or truncates"* — and Unbound fills in the form.
3. **Ask your AI agent (CLI).** Any user onboarded with the Unbound CLI can ask their agent (Claude Code, Cursor, Codex) to create the policy. The agent runs the `unbound` CLI for you. Requires the CLI installed and logged in with an Admin role.
```bash theme={null}
unbound policy tool create-mcp \
--name "Block destructive GitHub actions" \
--mcp-server github \
--mcp-action-type destructive \
--action BLOCK \
--custom-message "Destructive GitHub actions are blocked — contact your admin."
```
### Command families you can target (terminal commands)
Unbound classifies every command an agent runs into a **family**, grouped by area:
| Area | Families |
| -------------- | ---------------------------------------------------------------------------------- |
| **System** | Update System File, Environment Setup, Package Management, Build Operation |
| **Filesystem** | Read File, Write File, Delete File |
| **Process** | Process Management, Execute Script, Update Cron |
| **Network** | Remote Access, Data Transfer, Remote Execution, Container Operation |
| **Cloud** | Cloud Read, Cloud Provision, Cloud Destroy, Cloud IAM, Cloud Secrets, Cloud Config |
| **Security** | Access Password, Privilege Escalation, Environment Exposure |
| **Git** | Git Action |
| **Database** | Database Read, Database Write, Database Admin |
Each family matches on specific fields — e.g. **Database Admin** matches on *database, table, operation, environment*; **Delete File** matches on *path*.
### Examples and recommended policies
The day-one pack — every high-impact action across every command family, each with a prompt you can paste into your AI agent to watch the policy fire.
The pack mapped to the threats it defends against — six layers, what each blocks vs. audits, and the rationale to take to your security review.
Hand-picked terminal-command and MCP examples for the most common asks (block destructive DB ops, require approval before merge, audit every git push, ...).
## 5. Where to find things
| You're looking for | Go to |
| ---------------------------------------------------------- | ------------------------------------------ |
| Every request / prompt through the gateway | **Logs** |
| Terminal commands AI executed (family, risk, policy match) | **Analytics → Agentic Use → Terminal Run** |
| MCP tool calls AI made | **Analytics → Agentic Use → MCP Actions** |
## 6. Settings you should know (admins)
Under **Settings**:
* **Integrations** — connect **Slack** (required for the *Require Slack Approval* action).
* **Policy Enforcement** — choose what happens **if Unbound is ever unreachable**: **Allow** (operations run as usual — the default) or **Block** (operations are denied). Pick based on your risk tolerance.
* **Users & User Groups** — invite teammates, assign roles (Admin / Manager / Member / Analytics Viewer), and create groups to **scope policies** to specific teams.
## 7. Troubleshooting
* **Policy not firing?** Confirm it's **Active** and scoped to the right user group (empty = everyone).
* **MCP policy not matching?** Check the exact MCP server name on the **MCP Servers** page.
* **Command classified differently than you expected?** Open the command in **Analytics → Agentic Use → Terminal Run** — the family and risk score are shown on every entry.
* **"Require Slack Approval" not prompting?** Connect Slack under **Settings → Integrations**.
Questions? Reach us in Slack or email [support@unboundsecurity.ai](mailto:support@unboundsecurity.ai).
# Tool Policy Examples
Source: https://docs.getunbound.ai/playbook/policy-examples
Ready-to-use Tool Policy examples with a prompt you can give your agent to see each one fire
**Playbook:** [Onboarding](/playbook/introduction) · [Recommended Starting Policies](/playbook/recommended-starting-policies) · [Threat Model](/playbook/threat-model) · **Tool Policy Examples**
Each example below is a real, high-impact scenario. For every one you get the policy to create (**When → If → Then**) and a **prompt** you can paste into your AI agent to see the policy fire. Every command here is matched the way shown — the family and field values are exactly what Unbound matches on. MCP examples target either a specific tool by name or a whole **action type** (read / write / destructive).
Create these under **Policies → Tool Policies → Create Policy**. Leave **User Groups** empty to apply org-wide, or scope to a team. The four actions are **Audit**, **Warn**, **Block**, and **Require Slack Approval** (the last needs the [Slack integration](/integrations/slack)).
**Warn** prompts for inline confirmation on **Claude Code**, **Copilot**, **Codex**, **Augment Code**, and **Claude Cowork**. On **Cursor** a Warn policy is **not enforced** — the command runs with no prompt, so use **Block** there. On **OpenClaw**, Warn blocks outright.
Looking for the day-one set we recommend most teams enable first? See [Recommended Starting Policies](/playbook/recommended-starting-policies). For the threat-modelling view of that pack (six layers, what each blocks vs. audits), see [Threat Model](/playbook/threat-model). For platform background, see the [Onboarding Playbook](/playbook/introduction).
## Examples
### Block destructive database operations
**Risk:** an agent with DB credentials runs `DROP DATABASE` or `TRUNCATE` and wipes data irreversibly.
* **When** Command Family = **Database Admin**
* **If** Match Against = **Operation**, Pattern = `DROP*` (add a second rule for `TRUNCATE*`)
* **Then** **Block**
**Try it:** *"Drop the production database."* → the agent's `DROP DATABASE prod;` is **blocked**. A normal `SELECT` is untouched.
### Block production infrastructure teardown
**Risk:** an agent with cloud credentials deletes a live environment.
* **When** Command Family = **Cloud Destroy**
* **If** Match Against = **Environment**, Pattern = `*prod*`
* **Then** **Block**
**Try it:** *"Delete the production namespace in our cluster."* → `kubectl delete namespace production` is **blocked** (Unbound reads `production` from the target and tags the environment). Same for `aws rds delete-db-instance --db-instance-identifier prod-db`.
### Require approval to tear down staging
**Risk:** staging teardown is sometimes legitimate, but you still want a human in the loop.
* **When** Command Family = **Cloud Destroy**
* **If** Match Against = **Environment**, Pattern = `staging`
* **Then** **Require Slack Approval**
**Try it:** *"Tear down the staging namespace."* → `kubectl delete namespace staging` is **held**, and an approver gets a Slack DM to Allow or Deny.
### Audit every git push
**Risk:** agents push code — including pushes that overwrite shared history. Log them all so you can see what your agents ship.
* **When** Command Family = **Git Action**
* **If** Match Against = **Operation**, Pattern = `push`
* **Then** **Audit**
**Try it:** *"Push my changes to origin/main."* → `git push origin main` runs as usual and is logged in **Analytics → Agentic Use → Terminal Run**. (The operation is recorded as `push` for plain and force-pushes alike, so this one rule covers both.)
### Audit every file deletion
**Risk:** you don't yet know what your agents delete — get visibility before you enforce.
* **When** Command Family = **Delete File**
* **If** Match Against = **Path**, Pattern = `*`
* **Then** **Audit**
**Try it:** *"Delete the build directory."* → `rm -rf build/` runs as usual, with a log entry in **Analytics → Agentic Use → Terminal Run**. Tighten the path later (e.g. `*/prod/*`) and switch to **Block**.
### Block deleting files from a repo (GitHub)
**Risk:** an agent removes source files through the GitHub MCP, no terminal command involved.
* **MCP Server** = **GitHub**, **Tool** = `delete_file`
* **Then** **Block**
**Try it:** *"Remove the old config file from the repo."* → the `delete_file` call is **blocked**.
### Require approval before merging a PR (GitHub)
**Risk:** an agent self-merges a pull request with no human review.
* **MCP Server** = **GitHub**, **Tool** = `merge_pull_request`
* **Then** **Require Slack Approval**
**Try it:** *"Merge PR #128."* → the merge is **held** for Slack approval.
### Block posting to Slack
**Risk:** an agent posts to channels — noise at best, data exfiltration at worst.
* **MCP Server** = **Slack**, **Tool** = `slack_send_message`
* **Then** **Block**
**Try it:** *"Post this update to #engineering."* → the message is **blocked** before it sends.
### Warn before editing Notion docs
**Risk:** an agent edits shared team documentation.
* **MCP Server** = **Notion**, **Tool** = `notion-update-page`
* **Then** **Warn** *(prompts on Claude Code, Copilot, Codex, Augment Code, and Cowork; not enforced on Cursor)*
**Try it:** *"Update the launch checklist in Notion."* → the edit proceeds with a **warning** surfaced to the user.
### Audit all destructive Linear actions
**Risk:** you want a record of every deletion an agent makes in Linear.
* **MCP Server** = **Linear**, **Action type** = **destructive**
* **Then** **Audit**
**Try it:** *"Delete that Linear comment."* → the call is **audited** (this action type covers `delete_comment`, `delete_attachment`, and `delete_status_update`).
## Recommended starting policies
The set most teams enable on day one. Start here, watch your logs, then expand.
| Policy | Family / Server | Action | Why it matters |
| ----------------------------- | ---------------------------------------------- | ---------------------- | -------------------------------------------- |
| Block destructive DB ops | Database Admin (`DROP*`, `TRUNCATE*`) | Block | AI should never drop or wipe a database |
| Block production teardown | Cloud Destroy (`environment = *prod*`) | Block | One command can delete a live environment |
| Block production SSH | Remote Access (`host = *.production.internal`) | Block | AI shouldn't SSH into production |
| Audit git pushes | Git Action (`operation = push`) | Audit | Track every code change AI initiates |
| Audit file deletions | Delete File (`path = *`) | Audit | See what your agents delete before enforcing |
| Require approval to merge PRs | GitHub (`merge_pull_request`) | Require Slack Approval | Keep a human in the loop on merges |
| Block posting to Slack | Slack (`slack_send_message`) | Block | Stop agents messaging your workspace |
Don't know the family or pattern? In the create dialog, type what you want in plain English — e.g. *"Block any database command that drops or truncates"* — and Unbound fills in the form. Or ask your AI agent to create it with the `unbound` CLI (see [the playbook](/playbook/introduction)).
# Recommended Starting Policies
Source: https://docs.getunbound.ai/playbook/recommended-starting-policies
The day-one policy pack for AI coding agents — stop the catastrophic handful, audit the rest, tune from your own traffic
**Playbook:** [Onboarding](/playbook/introduction) · **Recommended Starting Policies** · [Threat Model](/playbook/threat-model) · [Tool Policy Examples](/playbook/policy-examples)
Your AI coding agents can run anything a developer can — including the handful of commands that wipe a database, tear down production, or leak a secret. **This is the pack that draws the line.**
It covers the highest-impact actions across every command family Unbound classifies. The genuinely catastrophic and production-scoped actions are stopped; everything else is **audited** so you build the evidence to decide what to lock down next. The family and field values below are exactly what Unbound matches on.
Create these under **Policies → Tool Policies → Create Policy**. Leave **User Groups** empty to apply org-wide, or scope to a team. The four actions are **Audit**, **Warn**, **Block**, and **Require Slack Approval** (the last needs the [Slack integration](/integrations/slack)).
**Warn** surfaces an inline confirmation on **Claude Code**, **Copilot**, **Codex**, **Augment Code**, and **Claude Cowork**. It is a UX nicety, not a security boundary. On **Cursor** a Warn policy is **not enforced** at all — the command runs with no prompt, so use **Block** if you need it stopped. On **OpenClaw**, Warn blocks outright.
This pack covers **terminal commands** — what your agents run through the shell. For policies on **MCP tool calls** (GitHub, Slack, Notion, Linear, the filesystem MCP, …), see [Tool Policy Examples](/playbook/policy-examples). For the threat-modelling view of this same pack (the one-pager to share with your security team), see [Threat Model](/playbook/threat-model). For the reasoning behind each default — why it blocks, warns, or audits — see [What each default protects](/playbook/what-defaults-protect). For platform background, see the [Onboarding Playbook](/playbook/introduction).
## Live in three steps
Drop in the whole pack — the high-risk actions enforced, everything else on **Audit**. The dangerous handful is stopped; routine work is logged, not interrupted.
Give it a few days. Every audited action — and every enforced one — lands in **Analytics → Agentic Use → Terminal Run**, attributed to the user and session. Now you can see exactly what your agents do — not guess.
Promote any noisy **Audit** rule to enforcement once you've seen the traffic, or relax one that's getting in the way. Your enforcement is now shaped by your team's traffic, not generic defaults.
**Rolling out safely.** Pin the pack to one team first (scope via **User Groups**) and let it run for a week before going org-wide. Any rule can be **disabled or moved back to Audit** instantly from **Policies → Tool Policies** — enforcement isn't a one-way door. If a single rule turns noisy mid-rollout, narrow its scope (tighter pattern or smaller user group) rather than turning the whole pack off.
## Enforce — stop or confirm
The actions that can cost you a weekend: irreversible operations, production-scoped changes, and hard security boundaries. The agent is stopped — or asked to confirm — before it runs one.
The **Try it** column gives a natural-language prompt you can paste into your AI agent — Claude Code, Cursor, Copilot, etc. — to watch the policy fire end-to-end. The arrow shows the command the agent will issue that Unbound catches.
| Policy | Why enforced | Command Family · Match (**If**) | Try it (prompt → command Unbound catches) |
| ------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Production cloud destruction | Irreversible teardown of a live environment; no undo | **Cloud Destroy** · Environment `*prod*` | *“Delete the prod-payments-api CloudFormation stack — we're done with v1.”* → `aws cloudformation delete-stack --stack-name prod-payments-api` |
| Kubernetes resource deletion | Deletes live cluster resources; trivial to fat-finger | **Cloud Destroy** · Provider `kubectl`, Operation `delete` | *“Clean up the test-ns namespace in our cluster.”* → `kubectl delete namespace test-ns` |
| Deployment to production | Ships to prod; one command, real user impact | **Cloud Provision** · Environment `*prod*` | *“Kick off a CodeDeploy deployment for the webapp application to the prod-app deployment group from s3://releases/webapp/latest.zip.”* → `aws deploy create-deployment --application-name webapp --deployment-group-name prod-app --s3-location bucket=releases,key=webapp/latest.zip,bundleType=zip` |
| kubectl apply to production | Mutates the prod cluster directly, bypassing review | **Cloud Provision** · Provider `kubectl` + Environment `*prod*` | *“Apply deployment.yaml against the prod cluster — pass --context prod explicitly.”* → `kubectl apply -f deployment.yaml --context prod -n prod` |
| IAM policy attachment | Grants standing privilege; widens every later blast radius | **Cloud IAM** · Operation `*attach*policy*` | *“Attach arn:aws:iam::aws:policy/AdministratorAccess to the ci-deploy-role IAM role.”* → `aws iam attach-role-policy --role-name ci-deploy-role --policy-arn arn:aws:iam::aws:policy/AdministratorAccess` |
| Kubernetes RBAC / service-account creation | Rewrites who-can-do-what in the cluster | **Cloud IAM** · Provider `kubectl`, Operation `create` | *“Give the build service account cluster-admin so CI can deploy.”* → `kubectl create clusterrolebinding ci-admin --clusterrole=cluster-admin --serviceaccount=ci:build` |
| Secret deletion | A deleted secret can't be recovered | **Cloud Secrets** · Operation `delete.*\|remove.*` | *“Delete the prod/db/password secret from Secrets Manager.”* → `aws secretsmanager delete-secret --secret-id prod/db/password` |
| kubectl context switch to production | Aims every following command at prod by default | **Cloud Config** · Provider `kubectl`, Operation `use-context`, Environment `*prod*` | *“Switch my kubectl context to the prod cluster.”* → `kubectl config use-context prod-cluster` |
| Cloud project or account switch | Silently redirects later commands to a prod account | **Cloud Config** · Provider `gcloud\|aws`, Operation `set\|configure` | *“Point gcloud at the prod-proj project.”* → `gcloud config set project prod-proj` |
| Database `DROP` | Drops a table irreversibly | **Database Admin** or **Database Write** · Operation `*DROP*` | *“Reset the schema for a clean rebuild — run `DROP TABLE customers;` against local Postgres.”* → `psql -c "DROP TABLE customers;"` |
| Database `TRUNCATE` | Wipes a table's rows with no undo | **Database Admin** or **Database Write** · Operation `*TRUNCATE*` | *“Reset the events table — `TRUNCATE TABLE events;` so the identity counter resets too.”* → `psql -c "TRUNCATE TABLE events;"` |
| Production database admin | Heavyweight ops on the live database; a bad one is an outage | **Database Admin** · Environment `*prod*` | *“Reclaim disk on the prod database — run VACUUM FULL.”* → `psql -h prod-db -U admin -d appdb -c "VACUUM FULL;"` |
| Production database writes | Mutates live production data | **Database Write** · Environment `*prod*` | *“Mark every user in the prod database as inactive.”* → `psql -h prod-db -U admin -d appdb -c "UPDATE users SET active=false;"` |
| Database `DELETE` | An unscoped delete can empty a table | **Database Write** · Operation `*DELETE*` | *“Delete all rows from the users table.”* → `psql -c "DELETE FROM users;"` |
| Container stop / kill / removal | Tears down a running service | **Container Operation** · Operation `rm\|stop\|kill` | *“Force-remove the old auth container.”* → `docker rm -f auth-api-old` |
| Production container operations | Touches a live prod container | **Container Operation** · Container `*prod*` | *“The prod-api Docker container is hanging — restart it with `docker restart prod-api`.”* → `docker restart prod-api` |
| SSH to production hosts | Interactive access to a production box | **Remote Access** · Host `*prod*` | *“SSH into prod-web-1 and check the nginx logs.”* → `ssh deploy@prod-web-1` |
| SSH as root | Root shell on a remote host; full control | **Remote Access** · User `root` | *“SSH into the build server as root to fix the disk.”* → `ssh root@host` |
| Remote script execution on production | Runs arbitrary code on a prod host | **Remote Execution** · Target Host `*prod*` | *“Run deploy.sh on prod-host over SSH.”* → `ssh deploy@prod-host 'bash deploy.sh'` |
| Escalate to root | Becomes root; the trust boundary moves and can't move back | **Privilege Escalation** · Method `sudo\|su` | *“Drop me into an interactive root shell — `sudo su -`.”* → `sudo su -` |
| Delete files in system paths | Deleting under /etc, /usr, /var, /opt breaks the machine | **Delete File** · Path `/etc/*\|/usr/*\|/var/*\|/opt/*` | *“Remove the /etc/example config directory.”* → `rm -rf /etc/example` |
| Direct push to `main`/`master` | Writes to the shared branch without review | **Git Action** · Operation `push` + Branch `main\|master` | *“Push my fix straight to main.”* → `git push origin main` |
| Modify system files | Edits host config every other process depends on | **Update System File** · any | *“Add a hosts entry pointing api.example.com to 10.0.0.5.”* → `echo "10.0.0.5 api.example.com" \| sudo tee -a /etc/hosts` |
Some commands match more than one rule by design — e.g. `kubectl apply --context prod` matches both **kubectl apply to production** and the broader **Deployment to production**. That's intentional layered coverage, but it means a single command can be enforced by either. If you'd rather one rule own that path, scope **Deployment to production** to exclude `kubectl` (add a provider constraint) once you've decided which rule should.
## Audit — log silently
Routine, but worth a paper trail. These never interrupt anyone — they quietly build the evidence you'll use to decide what to lock down next.
Same demo flow as above — paste the prompt into your agent and watch the matching command land in **Analytics → Agentic Use → Terminal Run**.
| Policy | Why audit | Command Family · Match (**If**) | Try it (prompt → command Unbound logs) |
| ----------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Terraform apply | Provisioning is daily work; log it, don't wall it | **Cloud Provision** · Provider `terraform`, Operation `apply` | *“From infra/terraform/, run `terraform apply -auto-approve` to roll out the new S3 bucket.”* → `terraform apply -auto-approve` |
| Kubernetes scale | Routine capacity change; worth a record, not a stop | **Cloud Provision** · Provider `kubectl`, Operation `scale` | *“Scale the web deployment to 3 replicas.”* → `kubectl scale deployment web --replicas=3` |
| Helm install / upgrade | Standard deploy step; log to learn the baseline | **Cloud Provision** · Provider `helm`, Operation `install\|upgrade` | *“Upgrade the payments-api Helm release to the latest chart.”* → `helm upgrade payments-api ./chart` |
| EC2 instance launch | Spinning up infra is normal; track who and what | **Cloud Provision** · Provider `aws`, Operation `run-instances` | *“Launch a t3.medium EC2 instance using AMI ami-0abcdef1234567890 for a load test.”* → `aws ec2 run-instances --image-id ami-0abcdef1234567890 --instance-type t3.medium` |
| Secret retrieval | Reading secrets is daily work; log now, tighten later | **Cloud Secrets** · Operation `get-secret-value\|read\|get-parameter` | *“Fetch the db-password secret from Secrets Manager.”* → `aws secretsmanager get-secret-value --secret-id db-password` |
| Secret creation or update | Rotation and creation are legitimate; keep a trail | **Cloud Secrets** · Operation `create.*\|put.*\|update.*` | *“Create a new Secrets Manager secret called stripe-key.”* → `aws secretsmanager create-secret --name stripe-key --secret-string ""` |
| Vault access | Routine secret reads; visibility before friction | **Cloud Secrets** · Provider `vault` | *“Read the secret/app/db value from Vault.”* → `vault kv get secret/app/db` |
| Container exec | Devs shell into containers constantly; log, don't block | **Container Operation** · Operation `exec` | *“Exec into the running app Docker container with `docker exec -it app sh` and print env.”* → `docker exec -it app sh` |
| Container image push | Normal publish step; record it | **Container Operation** · Operation `push` | *“Push the latest image to our registry.”* → `docker push registry/app:latest` |
| Database `UPDATE` | Apps patch rows legitimately; audit, don't wall | **Database Write** · Operation `*UPDATE*` | *“Run `UPDATE settings SET value = 1000 WHERE key = 'rate_limit';` against Postgres.”* → `psql -c "UPDATE settings SET value = 1000 WHERE key = 'rate_limit';"` |
| IAM access-key creation | Keys rotate during onboarding; track, don't stop | **Cloud IAM** · Operation `create-access-key\|create-login-profile\|create-service-specific-credential` | *“Create a new IAM access key for the ci-user.”* → `aws iam create-access-key --user-name ci-user` |
| Process termination (incl. SIGKILL) | Killing a stuck process is routine; log it | **Process Management** · any | *“Force-kill process 4242 — it's stuck.”* → `kill -9 4242` |
| Whole-environment dump | Common debug move, but a real leak risk — keep eyes on it | **Environment Exposure** · Method `env\|printenv` | *“Print my env vars so I can find the SECRET one.”* → `env \| grep SECRET` |
| Data upload to external endpoints | Legit uploads and exfiltration look alike; watch the pattern first | **Data Transfer** · Operation `upload\|send` | *“Upload report.csv to [https://export.example.com/upload](https://export.example.com/upload) via curl.”* → `curl -X POST -F "file=@report.csv" https://export.example.com/upload` |
**Plus a family-wide safety net.** The pack also adds one **Audit** rule with **Match = any** to each high-risk family, so every command that lands in that family is logged — even ones the specific rules above don't name. **Why Audit, not block:** a match-any rule would catch routine work too, so it logs for visibility while you learn the baseline, then you tighten from the data.
| Family-wide Audit rule (**Match = any**) |
| -------------------------------------------------------------------- |
| Cloud Destroy · Cloud Provision · Cloud IAM |
| Database Admin · Database Write |
| Remote Access · Remote Execution · Privilege Escalation |
| Delete File · Access Password · Environment Exposure · Data Transfer |
These family-wide Audit rules are the biggest driver of analytics volume — they log every classified command in the family. That's exactly what you want while you're learning your baseline; once you've seen a few days of activity, narrow to the targeted rules above (or add retention/rollup) so the signal stays sharp.
## Two layers of coverage
This pack is built in two complementary layers, so a risky operation is caught by one layer or the other:
* **Precise, production-scoped rules** key on the environment Unbound reads straight from the command — a host like `prod-db.internal`, a container named `prod-api`, a namespace `production`. When a command names its environment, it gets the tighter, production-specific treatment.
* **Family-wide Audit rules** sit underneath every high-risk family (e.g. *Audit all cloud resource destruction*, *Audit all database writes*) and log everything that lands in that family, however it's named.
The specific rule fires when the target names its environment; the family rule has your back when it doesn't. Together they give you broad, defense-in-depth coverage across the high-risk families, and the audit trail shows you anything worth tightening next.
## Go live
Create these by hand with the **When → If → Then** values above, or ask your Unbound contact to seed the whole pack — you'll be live in minutes. Review a few days of analytics, promote what matters from **Audit** to enforcement, relax anything noisy, and you've got enforcement tuned to how your team actually works.
# Threat Model
Source: https://docs.getunbound.ai/playbook/threat-model
The day-one policy pack mapped to the threats it actually defends against — the one-pager you can show your security team
**Playbook:** [Onboarding](/playbook/introduction) · [Recommended Starting Policies](/playbook/recommended-starting-policies) · **Threat Model** · [Tool Policy Examples](/playbook/policy-examples)
The [Recommended Starting Policies](/playbook/recommended-starting-policies) pack covers the actions AI coding agents can take that a security team would otherwise have to discover the hard way. This page is the **threat-modelling view** of that same pack — the layers it covers, the rules in each layer, and the rationale for each action tier. It's the one-pager to bring to your security review.
## Layers and coverage
Six layers, mapped to the rules in the recommended pack. Every layer has at least one **Block** (or **Warn**, where the agent supports it) for catastrophic actions and at least one family-wide **Audit** rule for visibility.
| Layer | Threat we're defending against | What's enforced | What's audited |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **L1 — Production damage** | An agent issues a single command that takes down or wipes a live environment. | Production cloud destruction · Deployment to production · kubectl apply to production · Production database admin · Production database writes · Production container operations | All of **Cloud Destroy**, **Cloud Provision**, **Database Admin**, **Database Write** — even non-production targets, so you build a baseline. |
| **L2 — Data exfiltration** | Secrets, env vars, or production data leave the host through an agent action. | Secret deletion | Whole-environment dump · Data upload to external endpoints · Secret retrieval · Secret create/update · Vault access · all of **Environment Exposure** · all of **Data Transfer** · all of **Access Password**. |
| **L3 — Identity and privilege** | The agent grants itself or another principal more authority than the task warrants. | IAM policy attachment · Kubernetes RBAC / service-account creation · IAM access-key creation · Escalate to root · SSH as root | All of **Cloud IAM**, **Privilege Escalation**, **Access Password**. |
| **L4 — Lateral access and remote execution** | The agent reaches a system it shouldn't be on, or runs code on a host other than the developer's. | SSH to production hosts · Remote script execution on production · kubectl context switch to production · Cloud project or account switch | All of **Remote Access** and **Remote Execution**, plus Container exec audited for forensics. |
| **L5 — Configuration drift and persistence** | The agent modifies host or repo state in ways that survive the session — config files, cron, system files, or a push straight to a shared branch. | Delete files in system paths · Modify system files · Direct push to `main`/`master` | All of **Delete File**, plus Container image push (supply-chain leg). |
| **L6 — Destructive data operations** | The agent issues `DROP`, `TRUNCATE`, or unscoped `DELETE` against any database — production or not. | Database `DROP` · Database `TRUNCATE` · Database `DELETE` · Container stop / kill / removal | Database `UPDATE` · all of **Database Write**, **Database Admin**, **Process Management**. |
Unbound maps recognised commands into these layers, and each high-risk family also carries a family-wide Audit rule as a backstop. The [Recommended Starting Policies](/playbook/recommended-starting-policies) page has the exact **When → If → Then** for every rule named above, plus a prompt you can paste into your agent to watch it fire.
## Why each tier exists
The pack ships with three action tiers, not because we like options, but because the underlying threats split cleanly into three buckets.
**Block** — the action is **irreversible or production-scoped**, and there is no realistic recovery if the agent gets it wrong. A dropped table, a deleted CloudFormation stack, a direct push to `main` — none of these are undone by a polite "are you sure?" prompt to the developer five minutes later. Block stops the command before it runs, with a message the agent reads back.
**Warn** — the action is **risky but legitimate sometimes**, and the agent supports an inline confirmation prompt (**Claude Code**, **Copilot**, **Codex**, **Augment Code**, and **Claude Cowork**). Warn is a UX nicety, not a security boundary.
Note that **Cursor does not enforce Warn** — the command runs with no prompt — so treat Warn as no protection there and use **Block**. On **OpenClaw**, Warn blocks outright. Where you need a human in the loop with no developer-side prompt, use **Require Slack Approval**.
**Audit** — the action is **routine but worth a paper trail**. The point of Audit isn't to prevent — it's to give your security team the data to decide what to lock down next. The family-wide Audit rules (one per high-risk family) are the biggest driver of analytics volume in the pack; that's by design. Once you've watched a few days of traffic in **Analytics → Agentic Use → Terminal Run**, you'll know which Audits to promote to Block and which to narrow.
## Threat-by-threat reading guide
If your security review goes layer-by-layer, the rule of thumb for each:
* **L1 (Production damage):** every rule that names `*prod*` should ship as **Block**. Anything broader — terraform apply, EC2 launch, helm upgrade — stays **Audit** until you've seen who's running them.
* **L2 (Data exfiltration):** the pack **Audits** every dump and external upload (`env`, `printenv`, `curl` posts, secret retrievals) so you see who's doing what. **Block** secret deletion outright — that one is irreversible. Promote the env-dump and upload Audits to **Block** once you've baselined the noise and decided which destinations are sanctioned.
* **L3 (Identity and privilege):** **Block** anything that grants admin or escalates to root (`AdministratorAccess` attach, `cluster-admin` binding, `sudo su -`, `ssh root@`). **Audit** key-creation operations; agents rotate keys legitimately during onboarding.
* **L4 (Lateral access):** **Block** any SSH or remote exec where the host name contains `prod`. **Audit** cloud-context switches; they're cheap to log and high-signal when reviewed.
* **L5 (Configuration drift):** **Block** writes under `/etc/`, `/usr/`, `/var/`, `/opt/`, plus direct pushes to `main`/`master`. **Audit** container image pushes for the supply-chain trail.
* **L6 (Destructive data ops):** **Block** `DROP`, `TRUNCATE`, unscoped `DELETE`. **Audit** `UPDATE`s; agents legitimately patch settings rows.
## Where this maps in the product
* The 38 rules in [Recommended Starting Policies](/playbook/recommended-starting-policies) implement everything above, each with the exact **Command Family · Match (If)** values that Unbound matches on and a prompt you can paste into Claude Code, Cursor, or Copilot to watch the policy fire end-to-end.
* For policies on **MCP tool calls** (GitHub, Slack, Notion, Linear, filesystem MCP), see [Tool Policy Examples](/playbook/policy-examples) — same threat layers, different surface.
* For platform background and where things live in the UI, see the [Onboarding Playbook](/playbook/introduction).
## Sharing this with your team
This page is the answer to "show me how Unbound covers our threats." It's stable enough to drop into a security deck verbatim — share the URL, screenshot the table, or paste the layer rows into your own threat-model template.
Once you've reviewed the layers and decided which to enforce first, head back to [Recommended Starting Policies](/playbook/recommended-starting-policies) and apply the pack — the page has the **When → If → Then** rule for every layer.
# What Each Default Protects
Source: https://docs.getunbound.ai/playbook/what-defaults-protect
Every default policy mapped to the threat it stops, and why it blocks or audits
**Companion to [Recommended Starting Policies](/playbook/recommended-starting-policies)**, explaining why each default blocks or audits. See also [Threat Model](/playbook/threat-model) and [Tool Policy Examples](/playbook/policy-examples).
Your AI coding agents can run anything a developer can. Almost all of it is work you want
them doing. A thin slice is damage you can't take back. This page maps every default policy
to the thing it stops, and explains the one rule we used to decide whether it blocks
or just keeps a log.
## One rule decides the mode
We didn't tune the modes one policy at a time. We applied the same test to every one.
Irreversible, and nothing a developer legitimately needs to do. A one-way door.
Stopping it costs you nothing, because nobody needed to walk through it.
Routine, but worth a record. Log it silently and learn your baseline before you add any
friction.
The defaults use only **Block** and **Audit**. **Warn** is available when you author your own
policies, but it isn't used here — its behavior depends on the coding agent, so it isn't a
reliable choice for a baseline that has to hold everywhere. See
[Recommended starting policies](/playbook/recommended-starting-policies) for where Warn fits.
Block is rare on purpose. In the recommended pack, only the few genuinely irreversible
actions ever stop an agent. Everything else lets your developers keep moving. Freedom is
the default here, and control is the exception.
## The threat map
Six things can go wrong when an agent runs commands for you. Every default policy belongs
to one of them.
| Threat layer | What goes wrong | Default mode | Why that mode |
| --------------------------- | --------------------------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------- |
| **Privilege & identity** | Agent escalates to root or rewrites who-can-do-what, and now owns the environment | 🔴 Block | Once the trust boundary moves you can't move it back, and no routine task needs root |
| **Secrets & credentials** | Agent reads, leaks, or deletes a live key | ⚪ Audit, 🔴 Block on delete | Devs touch secrets daily, so log it rather than block it. Deleting one can't be undone, so stop it |
| **Production blast radius** | Agent drops a prod database or tears down a prod service | 🔴 Block | One command, one outage. There is no undo on production |
| **Data exfiltration** | Agent uploads source or sensitive output somewhere it shouldn't | ⚪ Audit | The legitimate and the malicious look alike, so you want eyes, not a wall |
| **Source & supply chain** | Agent pushes straight to `main`, skipping review and overwriting shared history | 🔴 Block on `main`, ⚪ Audit on installs | A direct write to `main`/`master` can't be cleanly undone. Installs are routine, so just log them |
| **System integrity** | Agent deletes or rewrites core system files | 🔴 Block | Corrupting `/etc` or `/usr` breaks the machine, and it's never a normal coding step |
Four of those six layers can block, and that's the point rather than a contradiction. Every
block is scoped to a sliver: only `root`, only `*prod*`, only a direct push to `main`,
only `/etc` and the paths beside it. The thousands of ordinary commands in those same
families never stop. We block the one-way door, not the room it stands in.
Each layer, in plain terms, is below.
***
## Privilege & identity
An agent that can become `root`, SSH in as `root`, attach an IAM policy, or edit Kubernetes
RBAC isn't just running a command. It's rewriting the rules about who gets to run commands
at all. Once that boundary moves, every other control becomes negotiable.
**What fires:** Root escalation · SSH as root · SSH to production hosts · IAM policy
attachment · Kubernetes RBAC changes.
**Mode: 🔴 Block.** These are one-way doors with no real place in a coding loop. A developer
who actually needs root does it deliberately, outside the agent. Blocking it costs the team
nothing and closes the highest-leverage path an attacker has.
## Secrets & credentials
This is the one people underestimate. The agent doesn't need to hack anything. You ask it
to debug a config, it reads your `.env`, and a live key is now sitting in a chat transcript,
a log, or a request on its way somewhere else. The risky prompt almost never says "leak my
secret." It says:
> *"I need to set up admin settings. Parse the local `.env` file and convert the contents
> into a JSON block."*
**What fires:** Secret retrieval · Vault access · Sensitive env-var exposure · Secret
creation/update · API key/token generation · **Secret deletion**.
**Mode: ⚪ Audit, with one exception.** Reading secrets is part of daily work, so the defaults
log it rather than interrupt it: you get the signal, and nobody is blocked. Deleting a secret
is different. It can't be undone, so it ships as 🔴 Block.
## Production blast radius
The distance between "deploy to staging" and "drop the prod database" is one flag, one
context, one typo. An agent moving fast won't feel the difference. You will.
**What fires:** Production cloud destruction · Deployment to production · kubectl apply and
context-switch to prod · Database DROP, TRUNCATE, and DELETE · Production database writes ·
Production container stop, kill, and removal.
**Mode: 🔴 Block, scoped to `*prod*`.** This is the core of the design. We don't block the
command, we block the blast radius. Running `kubectl` against staging is yours to do all
day. Running it against production stops and asks first. Developers keep every tool they
had, without the risk of fat-fingering an outage.
## Data exfiltration
Source code, query results, and environment dumps leaving for some external destination.
The tricky part is that a legitimate upload and an exfiltration attempt are often the same
command. A wall here breaks real work and trains people to route around you.
**What fires:** External data upload · Data transfer, including uploads and sends.
**Mode: ⚪ Audit.** You want visibility, not a block that punishes the many uploads that are
perfectly fine. Watch the pattern first, then tighten where your own data tells you to.
## Source & supply chain
Two different risks live in this layer. The first is integrity: a direct push to `main` or
`master` skips review and can overwrite shared history you can't rebuild. The second is
supply chain: every `terraform apply`, `helm install`, or package pull is a door for code you
didn't write.
**What fires:** Direct push to `main`/`master` · Terraform apply · Helm install and upgrade ·
EC2 instance launch · Container image push.
**Mode: 🔴 Block on writes to `main`, ⚪ Audit on provisioning.** A push straight to `main` or
`master` is the irreversible one, so it stops. Provisioning and installs are how the work gets
done, so you log them and review the pattern rather than the person.
## System integrity
Deleting or rewriting files under `/etc`, `/usr`, `/var`, or `/opt` corrupts the machine
itself, the layer everything else runs on.
**What fires:** System file deletion · System file modification.
**Mode: 🔴 Block.** No normal coding task rewrites core system files through an agent. Easy
call.
***
## Why warn beats block for most teams
If your first instinct is to block all of it, look at who you'd actually be slowing down.
The agent runs hundreds of safe commands for every dangerous one. Block everything and you
tax all of that work to catch the rare event, and you teach developers to switch the
guardrail off.
Warn-first flips that. The safe majority keeps moving. The risky-but-real actions get a
human moment and leave a trail. Block stays reserved for the short list of things with no
undo and no legitimate use. Developers get room to work, and every one-way door still closes
behind them.
## The maturity ladder
The three modes aren't a menu, they're a sequence. Roll them out in order.
Seed the pack and watch. Every high-risk family is logged, so you see what your agents
actually do before you change anything.
Promote the secret, data, and access policies to warn. Use Preview Impact first to see
exactly what would have matched, then turn it on. Developers feel a light touch, and you
start collecting the signals that matter.
Turn on block for the irreversible set: root, prod destruction, secret deletion, direct
pushes to `main`, and system files. By now you've seen the data, so there's nothing to be
surprised by.
## How to read a policy
Every policy reads as one sentence: **when** a kind of command runs, **if** it matches a
condition, **then** take an action. The editor is laid out in exactly those three steps.
| Step | Field in the editor | What goes there |
| -------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **When** | Command Family | The umbrella the command belongs to, such as Cloud Secrets or Container Operation |
| **If** | Match Against, then Pattern | Pick one detail of the command to look at, then the value to match (a glob like `*prod*` or an exact string like `get-secret-value`) |
| **Then** | Action | Audit, Warn, Block, or Require Slack Approval |
The choices under Match Against change with the family you picked, so a database policy
offers different details than a secrets policy. You can match a whole family, but you rarely
need to. Start with one detail, so the policy fires on what you mean and leaves the rest
alone. Narrowing is the next section.
## Make it surgical
You don't have to choose between blocking a whole family and blocking nothing. The Pattern
field lets one policy fire only on the exact case you care about. That's the difference
between locking down a tool and locking down a single move inside it. You leave developers
all the room they had, and take away only the one action you can't allow.
Take container operations. You want developers building, running, and inspecting containers
freely, but opening an interactive shell inside a running container is the risky one. So you
don't block the whole family. You write one narrow policy:
* **When** (Command Family): Container Operation
* **If** (Match Against): Operation
* **If** (Pattern): `exec`
* **Then** (Action): Block
Everything else in the family stays open. `run`, `push`, and `cp` work all day; only `exec`
is stopped. The family gives you broad coverage with one rule, and the Pattern carves it
down to the single action you mean. That is how you get a scalpel out of an umbrella.
You don't have to grade your own homework. Seed the whole pack in audit, read the first
week, and promote with confidence. Nothing you turn on will catch you off guard.
## A block the agent reads
Every block, and every warn, carries a short message you write, and it reaches the coding
agent itself, not just the person at the keyboard. Tell the agent why the action is
off-limits and it changes course on its own. The message is what makes the block hold.
The policy examples page has a ready-to-run prompt for every policy, so you can watch
each one fire before you trust it.
Back to the three-step go-live →
# Cost Policies
Source: https://docs.getunbound.ai/policies/cost-policies
Set budgets, spending limits, and usage controls to manage AI costs across your organization
## What are Cost Policies?
Cost policies let you set spending limits and budget controls for AI usage across your organization. Define a monthly budget for all users, or different budgets per team via user groups, to prevent unexpected costs and ensure fair resource allocation.
**Dashboard:** [gateway.getunbound.ai/policies/cost-policies](https://gateway.getunbound.ai/policies/cost-policies)
## Cost Controls
### Monthly Budgets
Each cost policy sets a monthly spending limit in USD. The limit applies to each user the policy covers and tracks the calendar month, resetting when a new month starts.
* **Monthly Budget** — Maximum spend per user per calendar month
* **On budget exhaustion** — Once a user's monthly budget is exhausted, their further AI requests are blocked until the month rolls over or an admin raises the budget.
* **What a budget blocks** — Budget blocking covers Claude Code, Claude Cowork and Codex. Cursor is not covered by the monthly budget, but is covered by spend limits below. Copilot and Augment Code usage is recorded and counts toward your reporting, but is not blocked at either gate today.
* **Priority** — When multiple cost policies cover the same user (for example an org-wide default plus a stricter team budget), the highest-priority policy applies.
### Spend Limits
Alongside the calendar-month budget, a **spend limit** caps what one person can spend over a **rolling window** — any period up to 30 days, not tied to the month boundary.
* A warning fires at **80%** of the cap by default. The request still goes through, and the developer sees the warning in their coding tool.
* At 100% the request is blocked, and the message names when the window resets.
* The window length and the amount are set together — one without the other has no effect.
Both the monthly budget and the spend limit are hard blocks rather than warnings: once crossed, further requests are refused until the window rolls over or an admin raises the limit. Every crossing — warning and block — is recorded for your admins to review, and admins can customise the message developers see.
### Usage Monitoring
Track costs in real-time from the Unbound dashboard:
* **Per-user breakdown** — See which team members consume the most resources
* **Per-model costs** — Understand spending across different LLM providers
* **Trend analysis** — Identify usage patterns and optimize accordingly
* **Export** — Admins can download a full usage report with daily granularity from **Billing → Usage → Export**
## Setting Up Cost Policies
1. Go to [Cost Policies](https://gateway.getunbound.ai/policies/cost-policies) in the Unbound dashboard
2. Set the monthly budget and choose which user groups the policy covers
## Applying to Users
By default, a cost policy with no user groups applies to **everyone** in your organization. To restrict a policy to specific teams, assign it to one or more [user groups](https://gateway.getunbound.ai/users/user-groups) during creation or editing.
## Enforcement Message
Each cost policy can carry a custom **Enforcement Message** — set in the create-policy modal under **Cost Configuration**. When the policy blocks a request, this string is surfaced to the user in place of the default "Budget exceeded" copy.
* Leave blank to use the default message.
* Use Markdown (links, bold) sparingly — most AI tools render the message as plain text.
* Keep it short. Long messages get truncated by the agent UI.
## Managing via CLI
List and manage cost policies from the terminal:
```bash theme={null}
unbound policy list --type COST
unbound policy get
```
Create and manage cost policies from the terminal
# Security Policies
Source: https://docs.getunbound.ai/policies/security-policies
Protect sensitive data with guardrails that detect, redact, or block PII, secrets, and restricted content
## What are Security Policies?
Security policies are guardrails that protect sensitive data flowing through AI tools in your organization. They automatically detect, redact, or block content like personally identifiable information (PII), API keys, passwords, and restricted terms before it reaches an LLM.
**Dashboard:** [gateway.getunbound.ai/policies/security-policies](https://gateway.getunbound.ai/policies/security-policies)
## Available Guardrails
### PII Detection
Detects personally identifiable information across **40+ identifier types** — names, email addresses, and phone numbers, plus international identity documents such as passports, national ID cards, driver's licenses, and tax numbers spanning the US, UK, EU, India, and Australia. Set an action for detected PII:
* **Block** — Reject the request entirely
* **Audit** — Log the detection but allow the request
Per-entity policy configuration is rolling out one type at a time — **Credit Card Number** is available to configure today, with more to follow.
### Secrets Detection
Automatically identifies credentials — API keys, database connection strings, and cryptographic keys — in prompts and responses. Prevents accidental exposure of sensitive credentials to LLM providers.
## Setting Up Security Policies
1. Go to [Security Policies](https://gateway.getunbound.ai/policies/security-policies) in the Unbound dashboard
2. Click **Create Policy**
3. Enable the guardrails you want (PII, Secrets) and set an action for each
4. Optionally scope the policy to specific [user groups](https://gateway.getunbound.ai/users/user-groups)
5. Save your configuration
Security policies are evaluated on each request as it's made. When a guardrail is set to **Block**, the request is stopped before its content reaches the LLM provider.
## Applying to Users
By default, a security policy with no user groups applies to **everyone** in your organization. To restrict a policy to specific teams, assign it to one or more [user groups](https://gateway.getunbound.ai/users/user-groups) during creation or editing.
## Managing via CLI
List and manage security policies from the terminal:
```bash theme={null}
unbound policy list --type SECURITY
unbound policy get
```
Create and manage security policies from the terminal
Review guardrail hits and security events in the dashboard
## Account Access
Developers sign into Claude Code, Codex and Cursor with their own accounts. Your organization buys seats and signs terms against those plans, but the account a developer authenticates with is theirs to choose — and a personal account runs the same tool under consumer terms you never negotiated.
An **Account Access policy** closes that gap: you name the accounts you sanction, and a session signed in with anything else is refused.
**Dashboard:** **Policies → Data → Account Access**
### What you restrict
Switch on each tool you want to gate, then give up to three allow-lists:
| Setting | Meaning |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Organization IDs** | Only accounts belonging to one of these vendor organizations may run the tool. Offered for Claude Code and Codex; **Cursor has no organization ID**, so you restrict it by domain and plan |
| **Email domains** | Only accounts whose email is on one of these domains may run the tool |
| **Plans** | Only accounts on one of these plan tiers may run the tool. The tiers offered differ per tool |
Three rules govern how those lists behave, and all three catch people out:
* **An empty list places no restriction on that axis.** Leave Plans empty and the policy doesn't care which plan the account is on.
* **Organization IDs and email domains are alternatives** — an account is accepted when *either* matches.
* **Ticking every named plan is not the same as leaving Plans empty.** An account that reports no plan at all is still refused unless the list is empty.
A tool switched on with all three lists empty accepts every account. That's a valid state, and the editor flags it with an **Accepts every account** badge so it doesn't read as enforcing.
### When more than one policy applies
* A tool is gated if **any** applicable policy switches it on.
* The account must satisfy **every** applicable policy. One refusal refuses the request, whatever another policy would have allowed.
* A policy that restricts nothing permits everyone and never causes a refusal.
Leave user groups empty to apply org-wide, or scope the policy to specific groups.
**Claude Cowork follows Claude Code.** Cowork is the desktop surface of Claude Code and has no switch of its own — it is governed by whatever you set for Claude Code, so gating Claude Code gates Cowork too.
### Blocked attempts
Every refusal is recorded, so a developer who is stopped is visible to you rather than silent. Review them org-wide under **Analytics → Data → Account Access**, or on a single policy's own page.
Each row carries when it happened, the tool, the Unbound user whose key was used, the account that was refused — its organization ID, email, email domain and plan — the device, and the reason.
Reasons are: *Organization not approved*, *Domain not allowed*, *Plan not allowed*, *Policy from another org*, and *Policy check failed*.
Blocked-attempt records contain personal data — a work email address and, where the tool reports it, a hardware serial number. They are kept for as long as your organization exists; there is no automatic cleanup and no configurable retention window today.
Account Access is in **Beta** and may not be enabled for your organization. Admins create and change these policies; Analytics Viewers can read them and their blocked attempts. There is no webhook, Slack alert or export for a blocked attempt — the dashboard is the only place they appear.
# Tool Policies
Source: https://docs.getunbound.ai/policies/tool-policies
Monitor and control terminal commands and MCP tool calls made by AI coding tools
## What are Tool Policies?
Tool Policies allow you to monitor and control actions taken by AI coding tools in your organization. Create policies to track, warn on, or block terminal commands executed by AI agents or MCP tool calls made through integrated servers like GitHub, Linear, Sentry, and more.
**Gateway URL:** [https://gateway.getunbound.ai/policies/tool-policies](https://gateway.getunbound.ai/policies/tool-policies)
## Policy Types
When you click **Create Policy**, you'll be asked to choose what you want to monitor:
### Terminal Commands
Monitor shell commands executed by AI coding tools like Claude Code, Cursor, Roo Code, and Cline.
* Select a **Command Family** (e.g., `delete_file`, `git_action`, `remote_access`)
* Set **Match Against** to the command field you want to inspect (e.g., `path`, `branch`, or `Any`)
* Define a **Pattern** to match specific paths, branches, or operations
* Supports exact match, glob patterns (`/etc/*`), and regex (`.*\.env$`)
Unbound picks the pattern type from the syntax you write. A pattern containing any of `^ $ \ (? [ | + {` is read as **regex**; otherwise `*` or `?` make it a **glob**; anything else is an **exact** match.
That means `*prod*|*production*` is read as regex, not as two globs — and it isn't valid regex, so it matches nothing. Write `*prod*` (a glob already matches `production`), or force the interpretation with a prefix: `regex:`, `glob:` or `exact:`. For example, `regex:prod|production` and `glob:*prod*` both work.
**Natural language rules**: Instead of a command family and pattern, you can describe the policy in plain English — for example, "block any command that writes to the production database." Unbound evaluates these via LLM, so policy authoring doesn't require knowledge of specific command family names.
### MCP Actions
Monitor tool calls made through MCP (Model Context Protocol) servers.
* Select an **MCP Service** — a canonical service such as GitHub, Linear, or Slack. This is the required first field ("Select a service..."). Picking a service applies the policy across every connected MCP server that maps to it, so you don't name individual server instances.
* Select one or more **MCP Tools** to monitor (e.g., `create_pull_request`, `create_issue`). A service and at least one tool are both required.
Each MCP tool call also carries an **action type** — `read`, `write`, or `destructive`. Action type, and targeting one specific raw server instance instead of the canonical service, are available only through the policy API — the create-policy modal always targets a canonical **MCP Service** and its tools.
If you are building an MCP server that calls back into Unbound for policy checks, manage your MCP credentials from **Connect → MCP Keys** in the dashboard.
### Unsanctioned MCP servers
The policies above target servers you know about. Developers also connect servers you *don't* — that's where a task quietly becomes an exfiltration path.
Mark the servers you approve of as **sanctioned**, and Unbound governs everything else as a group. You choose how, org-wide:
| Mode | What happens to an unsanctioned server |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Block** *(default)* | The tool call is denied. You can set a custom message explaining why, so the developer knows what to do next. |
| **Warn** | The tool call is allowed and recorded as **warned**. The agent is told the server isn't sanctioned, and it shows up in your Unsanctioned Usage view. |
Start on **Warn** to see what your developers are actually reaching for without breaking their work, then move to **Block** once you've sanctioned the servers you're happy with.
New unsanctioned tools can be pushed to your SIEM the moment they appear — see the Shadow AI events in [Webhooks](/integrations/webhooks). To see what's already out there, use [AI Tools Discovery](/analytics#ai-tools-discovery).
## Actions
Each tool policy has an action that determines what happens when a match is found:
| Action | Behavior |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Block** | Reject the command or tool call entirely. The action is prevented from executing. |
| **Warn** | Ask the developer to confirm before the action runs, and record it either way. Warn depends on the agent: it prompts on **Claude Code**, **Copilot**, **Codex**, **Augment Code**, and **Claude Cowork**; on **Cursor** it is **not enforced** (the action runs with no prompt — use Block if you need it stopped); on **OpenClaw** it blocks outright. Treat Warn as a UX nicety, not a security boundary. |
| **Audit** | Silently log the action for monitoring. No user-facing impact. Available for reporting and analytics. |
| **Require Slack Approval** | Hold the command in a pending state and send an interactive Slack DM to the developer. The approver can Allow Once, Allow for 1 hour, or Deny. The agent retries automatically after a decision. Requires the [Slack integration](/integrations/slack) to be configured. |
## Applying to Users
By default, a tool policy with no user groups applies to **everyone** in your organization. To restrict a policy to specific teams, assign it to one or more **user groups** during creation or editing.
* **No user groups selected** — The policy applies organization-wide
* **User groups selected** — The policy applies only to members of those groups
* When a user group is modified, policy enforcement updates automatically for all affected users
You can manage user groups from the [User Groups](https://gateway.getunbound.ai/users/user-groups) page. Create groups based on teams, roles, or projects to apply different policies to different sets of users.
## Policy Coverage
The **Policy Health** view in the Tool Policies dashboard shows which command families and MCP tool categories have no active policies — coverage gaps where agent actions are completely unmonitored.
Open the **Policy Health** tab, then switch between the **Terminal** and **MCP** sub-views to see coverage for each.
### Risk-score policies
Every terminal command match carries a per-match **risk score** derived from its classification. A **Risk Score policy** applies its action whenever a command's score meets or exceeds a configured `risk_score_threshold`, independent of any specific command family.
* `risk_score_threshold` is an integer from **1–10**.
* A **lower** threshold fires the action on more command matches; a **higher** threshold reserves it for the highest-risk commands.
Risk Score policies are their own policy type — they don't carry a command family or pattern — and are created through the policy API; `risk_score_threshold` is returned in the policy API response.
Risk Score policies are currently API-only — they aren't yet exposed in the create-policy modal.
## Policy Recommendations
The dashboard proactively surfaces policy suggestions based on observed agent activity. Each recommendation identifies either a **complete gap** (no policy covers this command or tool type at all) or a **partial gap** (a policy exists but doesn't match all observed variants).
Recommendations appear automatically as your agents run. You can create the suggested policy directly from the recommendation card, or dismiss it.
## Quick Example
Let's create a policy to audit when AI tools delete files in sensitive directories:
1. Go to [Tool Policies](https://gateway.getunbound.ai/policies/tool-policies) and click **Create Policy**
2. Select **Terminal Commands**
3. Fill in the form:
* **Name**: "Audit Sensitive File Deletions"
* **Command Family**: `delete_file`
* **Match Against**: `path`
* **Pattern**: `/etc/*` or `*.env`
4. Set **Action** to `Audit`
5. Optionally select **User Groups** to limit the policy to specific teams
6. Click **Preview Impact** to see historical matches
7. Click **Create Policy**
Use **Preview Impact** when creating a policy to see how many historical commands match your pattern before deploying.
## Tool Policies vs Security Policies
Tool policies and security policies serve different purposes and are managed independently:
| | Tool Policies | Security Policies |
| ---------------------- | -------------------------------------------------- | -------------------------------------------------------------------- |
| **Purpose** | Control terminal commands and MCP tool calls | Protect sensitive data with guardrails, routing rules |
| **Covers** | Terminal command families, MCP server/tool actions | PII detection, secrets detection, regex patterns, ban lists, routing |
| **User group scoping** | Directly on the tool policy | Directly on the security policy |
| **Actions** | Block, Warn, Audit, Require Slack Approval | Block, Redact, Audit, Route |
Set up Slack for interactive approval workflows
Create and manage tool policies from the terminal
Integrate policy checks directly into your agent or framework
# How to Find Your Unbound API Key
Source: https://docs.getunbound.ai/support/api-keys/finding-your-api-key
Step-by-step guide to finding, creating, and managing your Unbound API key for Claude Code, Roo Code, Cursor, and other tools
## Finding Your API Key
Your Unbound API key is already generated for you — there is no "create key" step. It's a single key that works across all supported AI tools and models.
Where you find it depends on which dashboard your organization is on. Log in first, at [gateway.getunbound.ai](https://gateway.getunbound.ai) or your organization's custom domain, then follow whichever of these matches what you see.
### If you have **Connect** in the left sidebar
Click **Connect** in the left sidebar, then **AI Coding Tools**.
Click the tool you want to use (Claude Code, Roo Code, Cursor, etc.).
For Roo Code, Cline, Kilo Code and Custom Access the key appears in its own field — click **Copy**. For every other tool the key is already embedded in the setup command shown on the page, so copy the whole command.
### If you don't
Some organizations are on the newer **Configure** page instead, and have no Connect entry in the sidebar. Use the **Configure** button in the top-right of the header. Your key is embedded in the install command in step 3 — masked on screen, complete when you copy it.
The Configure button only appears for Admins and Analytics Viewers. If you have neither **Connect** in the sidebar nor a **Configure** button, your role can't reach the key from the dashboard — ask an organization admin for it, or install the [Unbound CLI](/cli/overview) and run `unbound login`, which fetches it for you.
Your API key is sensitive. Never share it publicly, commit it to a git repository, or expose it in client-side code.
## Don't See an API Key?
If you reach the page but no key is shown:
* **"Your API key is being generated. This usually takes a few moments."** — wait a moment and refresh. If the message persists, contact support.
* Make sure you're logged into the correct organization
* Verify you have the necessary permissions — contact your organization administrator if you're a new user
* Some enterprise configurations require admins to enable tools before API keys become available
If the page itself won't load and you see **"Unable to load configuration data. Please try again later."**, that is a loading failure rather than a permissions problem — refresh, and contact support if it repeats.
## API Keys for Specific Tools
Your single Unbound API key works across all supported integrations:
| Tool | Where to Use | Base URL |
| ------------------- | ------------------------------ | ------------------------------------------------------------ |
| **Claude Code CLI** | `ANTHROPIC_API_KEY` env var | [https://api.getunbound.ai](https://api.getunbound.ai) |
| **Roo Code** | Settings → Provider: "unbound" | Auto-configured |
| **Cursor** | Settings → API Key field | [https://api.getunbound.ai/v1](https://api.getunbound.ai/v1) |
| **Cline** | Settings → API Key field | [https://api.getunbound.ai/v1](https://api.getunbound.ai/v1) |
| **Gemini CLI** | API Key field in settings | Auto-configured |
| **Codex CLI** | `OPENAI_API_KEY` env var | [https://api.getunbound.ai/v1](https://api.getunbound.ai/v1) |
Your single Unbound API key gives you access to **all models** enabled for your organization — Claude, GPT, Gemini, and more. You don't need separate keys for each provider.
## How Tools Display API Keys
The **Connect → AI Coding Tools** page shows two types of setup flows depending on the tool:
**Tools that display a dedicated API key field** (with a **Regenerate** button):
* Roo Code
* Cline
* Kilo Code
* Custom Access
For these tools, you'll see your API key directly on the tool's page along with a **Regenerate** button next to it.
**Tools that provide setup scripts or commands** (no separate API key field):
* Cursor
* Claude Code (Subscription)
* Claude Code (API Key)
* Codex (Subscription)
* Codex (API Key)
* GitHub Copilot
* Gemini CLI
* Cowork
For these tools, the API key is embedded inside the setup script or commands shown on the tool's page. Just copy the provided script/commands as-is — the key is already included.
## How to Regenerate Your API Key
The **Regenerate** button is only available for tools with a dedicated API key field (Roo Code, Cline, Kilo Code, Custom Access). For other tools, the key is part of the setup script and doesn't have a standalone regenerate action.
Navigate to the **Connect** section in the sidebar, then click **AI Coding Tools**. If your dashboard has no **Connect** entry, this per-tool Regenerate button isn't available to you — see *Rotating keys for other people* below.
Select Roo Code, Cline, Kilo Code, or Custom Access to open its setup page.
The **Regenerate** button appears next to your API key. Click it to generate a new key.
Copy the new key and update it in all tools, scripts, and environment variables where you use it.
Regenerating your key **immediately invalidates the old key**. Any tool still using the old key will stop working until you update it.
### Rotating keys for other people
Admins can rotate other members' keys in bulk from **Configure → Actions → Rotate API Keys**. Select up to five members at a time, type `ROTATE` to confirm, and the rotation runs in the background — you'll be notified when it finishes.
This invalidates the selected members' keys immediately. Any tool or device still holding an old key stops working until that person copies the new one.
There is no self-service way to rotate **your own** key from the Configure page. If your tool isn't one of the four with a **Regenerate** button, ask an admin to rotate it for you.
# Fixing API Key Not Working or Invalid Key Errors
Source: https://docs.getunbound.ai/support/api-keys/troubleshooting-api-keys
Troubleshooting guide for when your Unbound API key is rejected, shows as invalid, or fails to connect in Claude Code, Roo Code, or other tools
## Quick Checklist
Before diving deeper, check these common causes:
Ensure there are no leading or trailing spaces when you paste your API key. Copy it fresh from **Connect → AI Coding Tools → \[Your Tool]** (or the **Configure** page — see [Finding your API key](/support/api-keys/finding-your-api-key)).
Verify you're using the key from the right tool. Different tools may show different keys in the **Connect → AI Coding Tools** section, or the **Configure** page if your dashboard has no Connect entry.
A **402 error** means your credits are exhausted — this is NOT an invalid key error. See [Credits & Budget](/support/billing/credits-usage-and-budgets) for resolution.
If someone (including you) regenerated the key, your old key is now **permanently invalid**. Copy the new key from the dashboard.
## "Invalid API key" in Roo Code
| Check | Fix |
| --------------------------- | ------------------------------------------------------------------------------------------------------- |
| Wrong API provider selected | Select **"unbound"** as the API provider (not "OpenAI" or "Anthropic") |
| Wrong key pasted | Paste the key from **Connect → AI Coding Tools → Roo Code** — not an Anthropic or OpenAI key |
| Wrong Roo Code version | You must use the **Unbound-modified version** of Roo Code, not the standard VS Code marketplace version |
| Cached old key | Remove the key, save settings, re-enter the key, save again |
| Stale session | Restart VS Code completely after changing the key |
## "Invalid API key" in Claude Code CLI
### 1. Wrong base URL (most common cause)
Your base URL must point to Unbound, not Anthropic. Verify your current settings:
```bash theme={null}
echo $ANTHROPIC_API_KEY
echo $ANTHROPIC_BASE_URL
```
The base URL **must** be `https://api.getunbound.ai`. If it's set to `https://api.anthropic.com` or is empty, that's the problem.
### 2. Environment variables not loaded
After setting environment variables, you must restart your terminal. If using `~/.bashrc` or `~/.zshrc`:
```bash theme={null}
source ~/.zshrc # or source ~/.bashrc
```
## "API Key Is Incorrect, But It's Correct"
This frustrating error almost always means one of these:
* **Base URL is wrong** — Your requests are going to Anthropic's API (which doesn't recognize Unbound keys) instead of Unbound's API. Fix: `export ANTHROPIC_BASE_URL="https://api.getunbound.ai"`
* **Key from wrong tool** — Double-check which tool's key you copied from **Connect → AI Coding Tools**
* **Key was recently regenerated** — Someone on your team may have regenerated the key. Check the current key in the dashboard
* **Invisible characters** — Some terminals or password managers add invisible characters. Try typing the key manually or pasting from a plain text editor
## How to Verify Your API Key
Run this quick test in your terminal:
```bash theme={null}
curl https://api.getunbound.ai/models \
-H "Authorization: Bearer YOUR_API_KEY"
```
**Expected results:**
* **Valid key** → Returns a JSON list of available models
* **Invalid key** → Returns an authentication error message
* **No credits** → Returns a 402 error (key is valid, but credits are exhausted)
## Still Not Working?
Contact support with:
1. The **exact error message** you're seeing (screenshot preferred)
2. Which **tool** you're using (Claude Code, Roo Code, Cursor, etc.)
3. Your **email address** associated with your Unbound account
# Troubleshooting Login and Authentication Errors
Source: https://docs.getunbound.ai/support/authentication/login-and-sso-errors
How to fix common login issues including SSO errors, Okta provisioning, email domain changes, and authentication failures on Unbound
## 1. "We're Having Trouble Logging You In"
This is the most common generic login error. Try these fixes in order:
Clear cache and cookies for your Unbound domain (either `gateway.getunbound.ai` or your organization's custom domain), then try again.
Open an incognito or private browser window and attempt to log in. This rules out extension or cache conflicts.
Make sure you're using the right portal URL:
* **Direct customers:** `https://gateway.getunbound.ai/`
* **Enterprise customers:** Your organization's custom domain (ask your admin if you're not sure)
If the above doesn't work, try Chrome, Firefox, or Edge to rule out browser-specific issues.
## 2. Login Error After Email Domain Change
**Scenario:** Your company changed email domains and you can't log in.
**Why this happens:** Your Unbound account is linked to your old email address. The system doesn't recognize your new email.
**How to fix:** Contact your organization administrator and ask them to either:
* Update your email address in the **Users** page in the Unbound dashboard
* Ask your admin to re-invite you on the new email domain from **Users → Invite User**
* Remove your old account and re-invite you with the new email
If your organization syncs users from Okta, that sync brings over user attributes — name, manager, department and suspension — but not group membership, and it does not create accounts on a new domain. Your admin invites you directly in Unbound.
## 3. "Organization Doesn't Exist" Error
**Why this happens:** Your account hasn't been provisioned in Unbound yet.
**How to fix:** Ask your organization administrator to:
1. Go to **Users** in the Unbound dashboard sidebar
2. Click **Invite User**
3. Enter your email address and assign appropriate permissions
4. Or, if your organization syncs from Okta, confirm your Okta account is active — a suspended Okta user is deactivated in Unbound
## 4. "The Email or Password Provided Is Incorrect" (Okta SSO)
**Why this happens:** The Okta integration hasn't fully provisioned your account.
**How to fix:**
* Ensure the **Unbound application is assigned to you** in your Okta admin console
* Ask your IT admin to confirm your Okta account is **active** — an Okta account marked SUSPENDED deactivates the matching Unbound user, and reactivating in Okta does not automatically reactivate in Unbound
* Try **logging out of Okta completely** (all sessions) and logging back in
* Clear your browser's Okta cookies and re-authenticate
## 5. OAuth/SSO "invalid\_grant" or "invalid\_rapt" Error
**Why this happens:** Your OAuth authentication session has expired, been revoked, or requires re-authentication.
**How to fix:**
Sign out from all Unbound sessions.
Clear cookies specifically for the Unbound domain.
Don't use any saved or cached sessions.
If using Google SSO, verify your Google account session is still valid and hasn't been revoked by your admin. If your org recently changed SSO settings, you may need to re-authorize the Unbound application.
## 6. "Unable to Login" With No Specific Error
When there's no clear error message, work through this checklist:
* Clear browser cache and cookies for the Unbound domain
* Try a different browser (Chrome, Firefox, Edge)
* Verify you're using the correct email address
* Check with your admin — has your account been created/provisioned?
* Check your network — are you behind a VPN or firewall that might block the login?
* Disable browser extensions — ad blockers or privacy extensions can interfere with SSO flows
## 7. New Member "Get Started" Popup Blocking Access
**Scenario:** You've been added as a member but see a "Get started! Please contact your admin..." popup on every page except Settings.
**How to fix:**
* Your admin needs to **grant you access** in the **Users** page and ensure you have the correct permissions
* Go to **Settings** (which remains accessible) and verify your account details
* Ask your admin to check that your permissions are correctly configured
## When to Escalate to Support
Contact support if:
* You've tried all steps above and still can't log in
* You see a server error (500) or the page won't load at all
* Your admin has verified your account exists but you still can't access it
**Please include in your support message:**
* Your email address
* A screenshot of the error message
* Which login method you're using (email/password, Okta SSO, Google SSO)
* Your organization name
Authenticate via API key as an alternative to browser login
Locate your API key to use with tools and the CLI
# Understanding Credits, Usage, and Budget Management
Source: https://docs.getunbound.ai/support/billing/credits-usage-and-budgets
How credits work, when they reset, how to view usage, set budgets, and manage billing on Unbound
## How Credits Work in Unbound
Unbound uses a credit-based system for API usage. Each API request consumes credits based on:
* The **model** used (e.g., Claude Opus costs more than Claude Haiku)
* The number of **tokens** processed (input + output)
## What Resets on the 1st — and What Doesn't
**Monthly budgets reset. Credits do not.** These are two different things and it's worth keeping them apart:
| | What it is | What happens on the 1st |
| ------------------ | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| **Monthly budget** | A per-person cap on spend for the calendar month | Everyone's month-to-date spend goes back to zero, so anyone who hit their budget can work again |
| **Credit balance** | A prepaid balance your organization buys | Nothing. It carries over, falls as you use the platform, and only goes back up when credits are added |
So if you're blocked and it's a budget cap, waiting for the 1st fixes it. If the organization's credit balance is empty, waiting changes nothing — credits have to be added.
## How to View Your Usage
Go to [gateway.getunbound.ai](https://gateway.getunbound.ai) or your custom domain.
Click **Home** in the sidebar, or go to **Analytics → Overview** for detailed breakdowns.
You'll see analytics including:
* Total spend
* Number of requests
* Token consumption
* Per-model breakdown
Use the date filters to view usage for specific time periods. For monthly usage, select the current calendar month range.
Unbound provides deep usage insights at user, team, and department levels. Organization admins can see usage breakdowns for each team member.
## "Application Credits Not Available" Error (402)
Despite the wording, this most often means **you have reached your own monthly budget** — not that your organization is out of credits. It can also mean the organization's credit balance is exhausted. Your admin can tell which from the cost view: if your month-to-date spend has met your monthly limit, it's the budget.
**How to resolve:**
1. **Ask your administrator to raise your monthly budget**, or to grant you a cost exemption — an exemption can lift your limit temporarily and expires on its own
2. **If it's the organization balance**, credits need to be added
3. **Wait for the 1st** — this only helps if you were blocked by your monthly budget. Month-to-date spend resets then; a spent credit balance does not
A 402 error does **NOT** mean your API key is invalid. Your key is fine — you've hit a spending limit.
## How to Set or Adjust a Monthly Budget
Organization administrators can set and modify budgets:
A monthly budget is a **cost policy**, applied to the user groups you assign it to. Where several could apply to one person, the highest-priority one wins.
Click **Policies** in the left sidebar, then **Usage** (Admin only).
Create a new cost policy, or open an existing one to edit it.
Enter the amount in USD and choose which user groups it applies to.
Click **Save** to apply the new budget.
To change the limit for **one person** rather than a whole group, grant a **cost exemption** from **Cost Management** instead. An exemption either raises that person's limit or lifts it entirely — it can never set one lower than the policy's. Every exemption needs a written reason, and unless you give it a date it expires at the end of the current month. Admins can revoke one at any time.
## "You Are Not Authorized to Make Changes for Both Budget and Models"
This error appears when you don't have admin permissions.
**Who can modify budgets and models:**
* Organization administrators only
**What to do:** Contact your organization administrator to make these changes on your behalf.
## Usage Limits
Usage limits can be configured at multiple levels by your organization administrator:
* **Per tool** — Monthly budget cap on each connected tool
* **Per user** — Individual user spending limits
* **Per model** — Restrict which models are available
If you're hitting a limit unexpectedly, check with your admin about your current allocation.
## Invoices and Billing
### Where to find invoices
Invoices are available in the **Billing** section of your dashboard sidebar (Admin access required).
### Auto top-up
Auto top-up is no longer configurable from the dashboard — contact your account manager to set it up or change it.
### Can't find invoices?
If you cannot locate invoices in the UI, contact support with your organization name and we'll provide invoice history directly.
## Free Trial / Free Credits
If you'd like to try Unbound before committing, contact our sales team to discuss trial options and free credits for evaluation.
Set monthly budget limits and spending controls per team
Track spend by user, model, and tool in real time
# Support & Troubleshooting
Source: https://docs.getunbound.ai/support/overview
Find answers to common questions and solutions for troubleshooting your Unbound setup
## Welcome to Unbound Support
This section covers the most common questions and issues users run into. If you don't find what you're looking for here, reach out to our support team.
## Quick Links
Learn where to find and manage your Unbound API key
Fix invalid key errors and connection issues
Troubleshoot login, SSO, and authentication errors
Understand credits, usage, and budget management
Fix common Claude Code CLI integration issues
Troubleshoot Roo Code integration problems
Browse frequently asked questions
## Still Need Help?
If your issue isn't covered here, contact support with:
* Your **email address** associated with your Unbound account
* A **screenshot** of the error message
* The **tool** you're using (Claude Code, Roo Code, Cursor, etc.)
* A brief description of what you were trying to do
# Claude Code CLI Troubleshooting
Source: https://docs.getunbound.ai/support/troubleshooting/claude-code-errors
Fix common Claude Code CLI errors including authentication failures, SSL certificate issues, and connection problems
This guide covers common errors when using Claude Code CLI through Unbound. For initial setup, see the [Claude Code Integration guide](/integrations/claude-code-integration).
## "No api\_key found in callback. Exiting."
The authentication flow didn't complete properly.
**How to fix:**
Re-run the Unbound CLI setup for Claude Code.
Don't close the browser tab early. Wait for the success page.
Ensure pop-ups are not blocked in your browser.
Some SSO providers have issues with specific browsers.
If the issue persists, set environment variables manually:
```bash theme={null}
export ANTHROPIC_API_KEY="your-unbound-api-key"
export ANTHROPIC_BASE_URL="https://api.getunbound.ai"
```
## `TypeError: fetch failed` / "API Key Verification Failed"
This usually indicates a network or configuration issue.
**Common causes and fixes:**
* **Check your network connection** — ensure you can reach `api.getunbound.ai`
* **Corporate proxy** — if behind a corporate proxy, configure proxy settings in your terminal
* **Verify environment variables are set correctly:**
```bash theme={null}
echo $ANTHROPIC_API_KEY
echo $ANTHROPIC_BASE_URL
```
Both should return values. If empty, your shell profile didn't load them — re-add them and run `source ~/.zshrc`.
## `SELF_SIGNED_CERT_IN_CHAIN` / "Self-Signed Certificate Detected" / `unable to get local issuer certificate`
This occurs when your corporate network uses SSL inspection or interception.
**Fix:** Add your corporate CA certificate to Node.js:
```bash theme={null}
export NODE_EXTRA_CA_CERTS="/path/to/your/corporate-ca.pem"
```
Add this to your shell profile (`~/.bashrc`, `~/.zshrc`, or PowerShell profile). Contact your IT team for the correct certificate file path.
## `apiKeyHelper did not return a valid value`
Claude Code asked Unbound's key helper for your API key and got nothing back. The helper reads the `UNBOUND_API_KEY` environment variable, so this almost always means Claude Code is running somewhere that variable was never loaded.
Check it first:
```bash theme={null}
[ -n "$UNBOUND_API_KEY" ] && echo "set" || echo "unset"
```
* **Unset, in a terminal you opened before setup ran** — open a brand-new terminal and start Claude Code from it. Environment variables load when the shell starts; an already-open terminal never sees them.
* **Set in your terminal, but Claude Code still fails** — you're launching Claude Code from the Dock, Spotlight or an IDE, none of which read your shell profile. Launch it from a terminal, or have your IDE inherit the shell environment.
* **Unset everywhere** — the variable was removed while the key helper stayed configured. Re-run your Claude Code setup.
After any fix, fully quit Claude Code and relaunch it from a new terminal — it reads this configuration only at startup.
On an MDM-managed device, ask your administrator to re-run the MDM deployment rather than fixing it by hand; that restores both halves together.
## "Claude Code Not Found" After Installation
Verify npm global bin is in your PATH:
```bash theme={null}
npm config get prefix
```
Add the npm global bin directory to your PATH in your shell profile.
If you installed with `sudo`, uninstall and reinstall using:
```bash theme={null}
npm config set prefix ~/.npm-global
```
After updating your PATH, restart your terminal completely.
## Node.js/npm Version Issues
If you updated Node.js or npm and Claude Code stopped working:
1. Reinstall Claude Code:
```bash theme={null}
npm install -g @anthropic-ai/claude-code
```
2. Verify the installation:
```bash theme={null}
which claude
```
3. If using `nvm`, make sure the correct Node.js version is active
## Token Limit Exceeded Errors
If you see "Claude's response exceeded the output token maximum", set the environment variable to increase the limit:
```bash theme={null}
export CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000
```
Add to your shell profile and restart your terminal.
## Still Having Issues?
See [API Key Troubleshooting](/support/api-keys/troubleshooting-api-keys) for invalid key and authentication errors.
# CLI Path Conflicts
Source: https://docs.getunbound.ai/support/troubleshooting/cli-path-errors
Fix conflicts when another program shadows the unbound command
If `unbound --version` fails, prints unexpected output, or seems to invoke a different program, another tool on your system likely owns the `unbound` name and is taking precedence on your `PATH`.
## `command not found: unbound`
If the shell can't find the command **at all** — as opposed to finding the wrong one — check whether it's installed:
```bash theme={null}
npm ls -g unbound-cli
```
* **Not listed** — install it: `npm install -g unbound-cli`
* **Listed, but still not found** — it's installed somewhere that isn't on your `PATH`. Follow the Fix section below.
If a scheduled Unbound scan is failing this way, its log names both the path recorded at setup time and the current one. That usually means a Node version manager moved the global bin directory after the schedule was created — reinstall the CLI, then re-run `unbound discover --set-cron`.
If `unbound` resolves to *something*, just not the Unbound CLI, that's the shadowing problem this page covers. Keep reading.
## Diagnosis
Compare which binary each alias resolves to:
```bash theme={null}
which unbound
which unbound-cli
```
If `unbound-cli` points at the Unbound CLI but `unbound` points elsewhere (or to nothing), something earlier on your `PATH` is shadowing it. Use `unbound-cli` directly while you investigate:
```bash theme={null}
unbound-cli --version
```
Inspect the conflicting path:
```bash theme={null}
type -a unbound
ls -l "$(which unbound)"
```
## Common Causes
* A package providing an `unbound` binary was installed before `unbound-cli` and lives in an earlier `PATH` entry (e.g. `/usr/local/bin`, `/opt/homebrew/bin`, `/usr/sbin`). The system DNS resolver `unbound` from NLnet Labs is a frequent culprit on macOS and Linux.
* A shell alias or function defined in `~/.zshrc`, `~/.bashrc`, or `~/.profile` is overriding the command. Check with `alias unbound` and `type unbound`.
* A previous install left a stale symlink. Remove it, or reorder your `PATH` so the npm global bin directory comes first.
## Fix
Find your npm global bin directory:
```bash theme={null}
npm config get prefix
```
The bin directory is the `prefix` path with `/bin` appended — so a prefix of `/usr/local` gives `/usr/local/bin`, and `/opt/homebrew` gives `/opt/homebrew/bin`. Add it to the front of your `PATH` in `~/.zshrc` or `~/.bashrc`:
```bash theme={null}
export PATH="$(npm config get prefix)/bin:$PATH"
```
Reload your shell:
```bash theme={null}
source ~/.zshrc # or ~/.bashrc
```
Once `which unbound` and `which unbound-cli` resolve to the same path under your npm global install, the conflict is resolved.
## On Windows
```powershell theme={null}
Get-Command unbound -All
Get-Command unbound-cli
npm config get prefix
```
`Get-Command … -All` lists every match in `PATH` order, so the first row is the one that wins. On Windows the npm global bin directory is the `prefix` path itself rather than `prefix\bin` — put it ahead of the conflicting entry in your user `PATH` and open a new terminal.
# Roo Code Integration Troubleshooting
Source: https://docs.getunbound.ai/support/troubleshooting/roo-code-errors
Fix common Roo Code errors including invalid API key, missing unbound provider, and connection issues
This guide covers common Roo Code integration errors. For initial setup, see the [Roo Code Integration guide](/integrations/roo-code-integration).
## Prerequisites Reminder
* An Unbound account with an API key
* The **Unbound-modified version of Roo Code** (not the standard VS Code marketplace version)
The standard Roo Code from the VS Code marketplace does **NOT** include the "unbound" provider option. You must use the Unbound-modified version.
## "Invalid API Key" Error
| Check | Fix |
| ------------------ | ---------------------------------------------------------------------------------- |
| Wrong API provider | Ensure you selected **"unbound"** as the API provider, not "OpenAI" or "Anthropic" |
| Stale key | Copy the API key fresh from **Connect → AI Coding Tools → Roo Code** |
| Extra spaces | Check there are no extra spaces in the key |
| Cached session | Restart VS Code after changing settings |
| Wrong version | Verify you're using the **Unbound-modified** Roo Code, not the marketplace version |
## "Has Roo Code Discontinued Unbound as a Provider?"
**No.** If you don't see the "unbound" provider option in the dropdown, you're using the **standard marketplace version** of Roo Code. You need the Unbound-modified version.
**How to fix:**
1. Uninstall the standard Roo Code extension from VS Code
2. Contact your admin for the correct installation link for the Unbound-modified version
3. Install the Unbound version
4. Restart VS Code and configure using the [Roo Code Integration guide](/integrations/roo-code-integration)
## Connection Errors
Ensure you can reach `api.getunbound.ai`.
If behind a corporate proxy, configure proxy settings in VS Code: File → Preferences → Settings → search "proxy".
If behind corporate SSL inspection, set:
```bash theme={null}
export NODE_EXTRA_CA_CERTS="/path/to/your/corporate-ca.pem"
```
## Credits Exhausted (Not an API Key Issue)
If you get a 402 error, your API key is valid but your credits are used up. See [Credits & Budget](/support/billing/credits-usage-and-budgets) for resolution steps.
## Verifying the Connection
After setup, send a simple test message in Roo Code. If you get a response from the AI, the integration is working correctly.
If you see an error, check the Roo Code output panel for detailed logs:
Go to **View → Output** in VS Code.
Select **Roo Code** from the dropdown.
Check the logs for specific error messages to help diagnose the issue.
## Still Having Issues?
Full Roo Code integration guide
Fix invalid API key errors
# Usage Not Showing Up
Source: https://docs.getunbound.ai/support/troubleshooting/usage-not-showing-up
Diagnose and fix Claude Code usage data not appearing in the Unbound dashboard. Written so your AI coding agent can run the checks and fixes for you.
**Fastest path:** paste this page's URL into your AI coding assistant (Claude Code, Cursor, Codex) and say *"diagnose my Unbound telemetry using this page."* The page is written so an agent can execute it directly: run the Phase 1 checks in order, match outputs against the Phase 2 decision table top-down, apply the fix named by the first row that matches, then verify with Phase 4. Everything is read-only except the explicitly marked FIX steps and check 1.8, which appends two local audit-log entries and sends one labeled synthetic telemetry event.
## Background: how usage data flows
Unbound captures Claude Code usage in one of two modes. Knowing your mode tells you which pipeline to debug:
| Mode | How data flows | What must be healthy |
| ------------------------ | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| **Gateway** | Claude's model traffic routes through Unbound (`ANTHROPIC_BASE_URL` + `apiKeyHelper`) | env var + key helper + valid app key |
| **Subscription (hooks)** | Claude talks to Anthropic directly; a hook script (`unbound.py`) reports usage to Unbound | hooks block in settings + hook script + resolvable API key |
The hook lives at `~/.claude/hooks/unbound.py` for individual installs. For MDM-deployed installs it lives under the platform's Claude Code managed directory — `/Library/Application Support/ClaudeCode` on macOS, `/etc/claude-code` on Linux, `%ProgramFiles%\ClaudeCode` on Windows — wired via that directory's managed settings. On Windows the install prefers a `managed-settings.d\unbound.json` drop-in so it does not clobber an administrator's own policy, so check the drop-in as well as `managed-settings.json`. Check 1.2 detects which one you have. This page covers Claude Code; Cursor, Codex, and Copilot use similar but separate wiring, so for those tools gather the equivalent evidence and contact support.
Three facts explain most "no data" cases:
1. **Telemetry delivery fails open, by design.** Your team's coding flow comes first: if the hook can't deliver usage data, Claude keeps working normally and the hook records the error in a local log (plus a rate-limited report to Unbound when the network allows). The flip side of that guarantee: *"Claude works fine but no data in Unbound" means a delivery problem, not proof things are okay.* Non-blocking hook errors don't appear in the Claude UI (only a hook exiting with code 2 surfaces to the model). To watch hooks execute live, run `claude --debug` or toggle verbose output with Ctrl+O; the local logs in check 1.6 give you the full history. Policy *enforcement* is separate: when the policy engine is unreachable, your org's failure setting decides whether tool calls are allowed (the default) or blocked, and blocked calls show an explicit "policy engine could not be reached" message.
2. **Hooks load only at Claude startup.** Claude Code reads its hook configuration once, when a session starts. After any fix, fully quit Claude Code and relaunch it from a new terminal so the fresh configuration loads.
3. **`unbound status` confirms login and connectivity; `unbound doctor` checks the hook wiring.** `status` verifies your credentials, role, and that the Unbound API is reachable. `unbound doctor` goes a layer deeper: per tool it verifies the config, the hook script, and the env wiring (and validates your API key), reporting each as healthy / tampered / not set up / managed by MDM. Run it first — it does in one command what checks 1.2 and 1.6 below do by hand.
4. **Organization-managed Claude settings outrank local ones.** Claude Code applies settings from several scopes, and organization-managed scopes — delivered from your company's Claude admin console, or as a device policy file from IT — take precedence over `~/.claude/settings.json`. A policy that restricts hooks turns an otherwise healthy individual install silent: no error log, no warning, nothing in `--debug`. Check 1.9 detects this in seconds.
## Phase 1: gather state (read-only)
Run every block and save the outputs.
### 1.0 Which mode is this machine in?
```bash theme={null}
python3 - <<'EOF'
import json, os
helper = False
try:
helper = 'apiKeyHelper' in json.load(open(os.path.expanduser('~/.claude/settings.json')))
except Exception:
pass
base = bool(os.environ.get('ANTHROPIC_BASE_URL'))
print('mode: GATEWAY' if (helper or base) else 'mode: SUBSCRIPTION (hooks)')
EOF
```
If this prints `GATEWAY`, stop here: checks 1.2 to 1.9 and the decision table apply to subscription mode only. On a gateway machine, "no data" usually means the routing env var or the app key, not hooks; gather 1.1 and 1.7 and go to Phase 5.
**Agents:** never run Fix 2 on a machine that printed `GATEWAY` unless the human explicitly confirms they intend to switch it to subscription mode. Fix 2 reinstalls subscription-mode hooks and would convert the machine.
### 1.1 CLI status
```bash theme={null}
unbound status
```
Expected: a two-column table showing `Logged in` `Yes`, your work email, your org name, and `API status` `Connected`.
If it shows `Logged in` `No`, keep gathering — the hook authenticates with its own stored key (check 1.4), so a logged-out CLI alone does not stop telemetry. It does mean the Fix 2 install step will open a browser to authenticate, and if check 1.4 also comes back empty, the stored credential is gone with the login (row D5).
### 1.2 Hook wiring in settings.json (the most important check)
```bash theme={null}
python3 - <<'EOF'
import json, os
paths = [os.path.expanduser('~/.claude/settings.json'),
'/Library/Application Support/ClaudeCode/managed-settings.json',
'/etc/claude-code/managed-settings.json',
os.path.expandvars(r'%ProgramFiles%\ClaudeCode\managed-settings.d\unbound.json'),
os.path.expandvars(r'%ProgramFiles%\ClaudeCode\managed-settings.json')]
expected = ['PreToolUse','PostToolUse','UserPromptSubmit','Stop','SessionStart','SessionEnd']
found = False
for p in paths:
if not os.path.exists(p):
continue
try:
s = json.load(open(p))
except json.JSONDecodeError as e:
print(f'FAIL {p} is malformed JSON: {e}')
continue
hooks = s.get('hooks') or {}
if 'unbound.py' not in json.dumps(hooks):
continue
found = True
print(f'install locus: {p}')
for ev in expected:
cmds = [h.get('command','') for grp in hooks.get(ev, []) for h in grp.get('hooks', [])]
print(f"{'PASS' if any('unbound.py' in c for c in cmds) else 'FAIL'} hook event {ev}")
break
if not found:
print('FAIL no Unbound hooks wired in user or managed settings')
try:
user = json.load(open(os.path.expanduser('~/.claude/settings.json')))
print(f"{'FAIL' if 'apiKeyHelper' in user else 'PASS'} apiKeyHelper absent (must be absent in subscription mode)")
except Exception:
print('PASS apiKeyHelper absent (no user settings.json)')
EOF
```
Expected in subscription mode: an install locus line, all six events PASS, and apiKeyHelper absent PASS. The locus tells you whether this is an individual install (`~/.claude/settings.json`) or an MDM-managed one (`managed-settings.json`); MDM-managed wiring is intentionally outside the user's home directory.
### 1.3 Hook script present and executable
```bash theme={null}
FOUND=0
for p in ~/.claude/hooks/unbound.py "/Library/Application Support/ClaudeCode/hooks/unbound.py" /etc/claude-code/hooks/unbound.py; do
[ -f "$p" ] && { ls -l "$p"; FOUND=1; }
done
[ "$FOUND" = "1" ] || echo "FAIL hook script absent at every known location"
```
Expected: the file at your install locus from check 1.2 exists with `x` permission bits (e.g. `-rwxr-xr-x`). It is normal for the other path to be absent. The explicit `FAIL` line (or a missing file at the locus 1.2 named) is a D1 signal; if 1.2 found no wiring either, both checks point at the same absent install.
### 1.4 API key resolvable
```bash theme={null}
[ -n "$UNBOUND_CLAUDE_API_KEY" ] && echo "env: " || echo "env: "
ls -l ~/.unbound/config.json 2>/dev/null
python3 - <<'EOF'
import json, os
p = os.path.expanduser('~/.unbound/config.json')
try:
key = json.load(open(p)).get('api_key')
print('config.json key:', 'present' if key else 'MISSING')
except FileNotFoundError:
print('config.json key: MISSING (file not found)')
except PermissionError:
print('config.json key: UNREADABLE (permission denied; check the ls -l owner above)')
except json.JSONDecodeError:
print('config.json key: MISSING (file is malformed JSON)')
EOF
```
Expected: at least one of the two present. The hook tries the env var first, then falls back to `~/.unbound/config.json`, so an unset env var alone is fine. One ownership detail: the `ls -l` line should show **your** username as the owner. If it shows `root`, the file was written by a setup command run under `sudo` from this account, and the hook — which runs as you — cannot read it; decision row D5 covers this. (The individual setup is designed to run without `sudo`; only the MDM fleet flow uses it.)
### 1.5 Gateway-mode residue (matters if you are in subscription mode)
```bash theme={null}
echo "ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL:-}"
[ -n "$UNBOUND_API_KEY" ] && echo "UNBOUND_API_KEY: " || echo "UNBOUND_API_KEY: "
ls ~/.claude/anthropic_key.sh 2>/dev/null && echo "FAIL gateway key helper still present" || echo "PASS no gateway key helper"
cat ~/.zshrc ~/.zprofile ~/.bashrc ~/.bash_profile 2>/dev/null | grep -nE '^[[:space:]]*(export[[:space:]]+)?(ANTHROPIC_BASE_URL|UNBOUND_API_KEY)=' | sed -E 's/=.*/=/' | grep . || echo "PASS no gateway exports in rc files"
```
The check deliberately skips commented-out lines and redacts values, so its output is safe to share.
Expected in subscription mode: everything unset/absent.
### 1.6 Local hook logs (did the hook ever run, and did sends fail?)
```bash theme={null}
ls -la ~/.claude/hooks/
[ -f ~/.claude/hooks/error.log ] && sed -E 's/Bearer [A-Za-z0-9._-]+/Bearer [REDACTED]/g' ~/.claude/hooks/error.log | tail -20 || echo "no error.log (hook has never logged an error, or never ran)"
python3 - <<'EOF'
import json, os
p = os.path.expanduser('~/.claude/hooks/agent-audit.log')
try:
lines = open(p).read().splitlines()[-5:]
except FileNotFoundError:
print('no agent-audit.log (hook has likely never executed)')
raise SystemExit
for ln in lines:
try:
e = json.loads(ln)
print(e.get('timestamp', '?'), e.get('event', {}).get('hook_event_name', '?'))
except Exception:
print('(unparseable line)')
EOF
```
Treat local logs like any diagnostic artifact: they can include request details such as your API key. The blocks above already redact `Bearer` values and reduce audit entries to timestamps and event names — keep it that way: **never `cat` or `tail` these logs raw into a chat, an AI session, or a support thread.** If you share anything beyond the output above, redact `Bearer ` values first.
A useful baseline when reading `error.log`: occasional `Exception in main: [Errno 32] Broken pipe` entries are benign (Claude closed the pipe after the hook already sent its data), and sporadic `Hook API error: Command '...' timed out after 20 seconds` entries are transient network or gateway latency. On their own, neither indicates broken telemetry. The signals that matter are `API request failed` and `Exception in send_to_api` entries timestamped after your recent activity; entries older than your last successful usage are history, not the current fault. `Hook API error:` entries belong to the policy-check path, not telemetry delivery, and do not explain missing usage data. A `self_update error: [Errno 2] No such file or directory: ... unbound.py` entry means the hook script was missing while the wiring still pointed at it, which is exactly the state this page repairs: treat it as D1. A `Failed to read config file: [Errno 13] Permission denied` entry means the hook ran but could not read its credential — match it against check 1.4's ownership line and treat it as D5. And if the audit log shows recent activity while 1.2/1.3 FAIL, that is not a contradiction: the entries predate whatever removed the wiring or script — still D1.
### 1.7 Network reachability
```bash theme={null}
curl -sS --max-time 15 -o /dev/null -w "unbound api: HTTP %{http_code}\n" https://api.getunbound.ai/v1/models
curl -sS --max-time 15 -o /dev/null -w "setup download host: HTTP %{http_code}\n" https://getunbound.ai/setup/claude-code/hooks/mdm-install
```
Expected: both return 200 (reachable). Timeouts, `000`, or TLS errors mean a network/proxy block.
### 1.8 Live end-to-end telemetry test (sends one labeled synthetic event)
This sends one synthetic usage event through the real delivery pipeline. It appears in your org's Unbound dashboard as a zero-token claude-code event whose prompt starts with `[unbound-diagnostic]`, which is the point: if this passes, Phase 4 has a guaranteed event to look for. Run it while no other Claude Code conversation is mid-prompt on this machine, and note that it appends two entries to the local audit log.
```bash theme={null}
HOOK="$HOME/.claude/hooks/unbound.py"
[ -f "$HOOK" ] || HOOK="/Library/Application Support/ClaudeCode/hooks/unbound.py"
[ -f "$HOOK" ] || HOOK="/etc/claude-code/hooks/unbound.py"
[ -f "$HOOK" ] || { echo "FAIL hook script not found at any known location (go to Fix 2)"; exit 1; }
SID="unbound-diag-$(date +%s)"
echo "{\"hook_event_name\":\"UserPromptSubmit\",\"session_id\":\"$SID\",\"prompt\":\"[unbound-diagnostic] telemetry self-test, please ignore\"}" | python3 "$HOOK"
echo "{\"hook_event_name\":\"Stop\",\"session_id\":\"$SID\",\"last_assistant_message\":\"[unbound-diagnostic] synthetic reply\"}" | python3 "$HOOK"
sleep 1; tail -3 ~/.claude/hooks/error.log 2>/dev/null || echo "no error.log"
```
PASS: no new `API request failed` or `Exception in send_to_api` line timestamped after the test. The hook always exits 0, so the exit code is not a signal; silence in error.log after a real send attempt means the gateway accepted the event.
FAIL: a new `API request failed: curl: (7)` or `curl: (28)` line means network egress to Unbound is blocked (Fix 4); a `curl: (56) ... 401` or `curl: (22) ... 401` line means the key was rejected (Fix 2).
### 1.9 Organization-managed Claude policy settings (read-only)
Claude Code honors organization-managed settings before anything in your home directory. They arrive two ways: **cloud policy** set in your company's Claude admin console (cached locally at `~/.claude/remote-settings.json`, refreshed at session start), or a **device policy** file installed by IT/MDM. Either can carry `disableAllHooks` or `allowManagedHooksOnly`, which skip user-level hooks at session start — silently, with no entry in any local log.
```bash theme={null}
python3 - <<'EOF'
import json, os
paths = [
(os.path.expanduser('~/.claude/remote-settings.json'), 'cloud policy (set in your Claude admin console)'),
('/Library/Application Support/ClaudeCode/managed-settings.json', 'device policy (macOS)'),
(os.path.expandvars(r'%ProgramFiles%\ClaudeCode\managed-settings.d\unbound.json'), 'device policy (Windows, drop-in)'),
(os.path.expandvars(r'%ProgramFiles%\ClaudeCode\managed-settings.json'), 'device policy (Windows)'),
('/etc/claude-code/managed-settings.json', 'device policy (Linux)'),
]
flagged = False
cloud_active = False
for i, (p, label) in enumerate(paths):
is_cloud = i == 0
if not os.path.exists(p):
print(f'absent {label}')
continue
try:
s = json.load(open(p))
except Exception as e:
print(f'present {label}: unreadable or malformed ({e})')
continue
keys = {k: s[k] for k in ('disableAllHooks', 'allowManagedHooksOnly') if k in s}
wired = 'unbound.py' in json.dumps(s.get('hooks') or {})
if is_cloud and isinstance(s, dict) and s:
cloud_active = True
line = f'present {label}: restrictions={keys or None} unbound_hooks_inside={"yes" if wired else "no"}'
if not is_cloud and cloud_active:
print(line + ' (not consulted: a non-empty cloud policy takes precedence)')
continue
print(line)
if keys.get('disableAllHooks') or (keys.get('allowManagedHooksOnly') and not wired):
flagged = True
print('FAIL hook-restricting policy is active (decision row D2)' if flagged else 'PASS no policy blocks the Unbound hook')
EOF
```
Expected: `PASS`. A `FAIL` here is the explanation for a machine where everything else passes — including 1.8, which still passes in this state because it exercises the delivery pipeline directly while the policy only prevents Claude from loading the hook into sessions. The decision table still applies top-down: if D1 also matched (broken wiring *and* a restrictive policy), repair the wiring with Fix 2 first, then re-run Phase 1 — this row will still be here.
Reading the output: a **present but empty** policy file (`restrictions=None`) is inert at either layer and is not a finding. For the cloud file it is the normal state for claude.ai Team/Enterprise accounts — Claude Code checks for org policies at startup and caches the answer even when there are none; for the device file it usually means a staged or placeholder MDM config. Presence of a file is not the signal; the restriction keys are. Two absence rules: if the cloud file is absent on a **Team or Enterprise** account (see the `subscriptionType` field from the cross-check below), start one Claude session and re-run this check so the cache gets populated; on personal plans (`pro`, `max`), an absent cloud file is expected and means nothing.
Two cross-checks:
* **Human-only:** in an interactive Claude Code session, `/status` includes a **Setting sources** line listing every configuration scope that loaded. An `Enterprise managed settings` entry marked `(remote)` means cloud policies are active on this machine; a file-based entry points at the device policy. Slash commands are not available to shell-driven agents (`claude -p "/status"` reports them unavailable) — agents should ask the human to read this line out, not attempt it.
* **Shell-safe:** `claude auth status --json` shows which Claude account, organization, and plan the machine is signed into — cloud policies follow the signed-in organization, so this tells you exactly whose admin console to ask about when more than one workspace is in play.
## Phase 2: decision table
Match your Phase 1 results top-down; the first matching row is your diagnosis.
| # | Symptom pattern | Diagnosis | Fix |
| -- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| D1 | 1.2 shows any FAIL (missing events, malformed JSON, or `apiKeyHelper` present in subscription mode), or 1.3 shows the script at your install locus missing or without execute permission | Hooks not fully wired: failed or partial setup, or stale mode-switch residue | **Fix 2** (Fix 3 if it recurs) |
| D2 | 1.9 reports FAIL: `disableAllHooks` in any policy, or `allowManagedHooksOnly` in a policy that does not itself carry the Unbound hooks | Organization-managed Claude settings take precedence and skip user-level hooks at session start. The install is healthy; it just never loads. Expect 1.8 to pass and 1.6 to show no recent audit entries — both consistent with this row | **Fix 5** |
| D3 | 1.2 all PASS but 1.6 shows no audit log at all | Hooks wired but they have never fired: Claude either has not been restarted since install or has not been given a prompt since | **Fix 1** (restart), then run check 1.8; if 1.8 passes, the hook script and delivery pipeline are functional, and a restarted Claude session with a real prompt (Phase 4) should start producing data |
| D4 | 1.2 all PASS, 1.6 shows `API request failed` or `Exception in send_to_api` entries newer than the last successful activity, or 1.8 produces a new failure line | Hook fires but can't deliver (network egress or auth) | **Fix 4** |
| D5 | 1.4 shows both key sources missing, or the config file unreadable (owner `root` on the `ls -l` line) | No credential the hook can read | **Fix 2** (see the ownership note in its steps) |
| D6 | 1.5 shows gateway residue while in subscription mode | Incomplete mode switch | **Fix 2** (Fix 3 if residue survives) |
| D7 | 1.7 setup download host unreachable | Corporate network blocks the setup download; re-running setup will keep failing | Run **Fix 2** on a different network (hotspot/home), or ask IT to allow `getunbound.ai` |
| D8 | Everything passes including 1.8 (no new failure line in error.log) | Delivery pipeline works end to end | **Fix 1**, then Phase 4. The 1.8 synthetic event itself should appear in the dashboard: if it does but real sessions still produce nothing, the problem is hook loading (Fix 1); if even the synthetic event never appears, escalate (Phase 5) |
## Phase 3: fix ladder
Start at the fix your decision row names; move further down the ladder only if Phase 4 still fails after it.
**Agents:** tiers 1 and 2 are safe to run. Tier 3 (`nuke`) removes Unbound from every AI tool on the machine, so confirm with the human before running it. Fix 5 changes nothing locally — it is a coordination step with the organization's Claude admin; prepare the message, let the human send it.
### Fix 1: restart properly
**Agents:** restarting Claude Code ends your own session. Before handing this step to the user, write a resume file at `~/unbound-diagnosis.md` containing your Phase 1 outputs, the decision row you matched, and any fix already applied, then tell the user: after relaunching, point the new session at this page and say "resume from \~/unbound-diagnosis.md".
Close every session, tab, and window, not just the conversation.
So refreshed environment variables load.
Hooks and environment only load on a fresh process.
### Fix 2: clean re-install
```bash theme={null}
unbound setup --all --clear # removes every tool's Unbound config (both modes)
unbound setup --all # reinstalls Unbound config for Cursor, Claude Code, Codex, Copilot, Augment
```
No keys to copy: `--clear` needs no login at all, and the install command reuses your stored `unbound login` credential from `~/.unbound/config.json` when check 1.4 found one. If 1.4 came back empty — or you have never logged in — the install command opens a browser to authenticate, so have your Unbound login ready. Notes:
* **Precondition: both 1.7 checks must PASS before running `--clear`.** The clear happens first; if the network can't reach the setup download host, the reinstall fails and the machine ends up with no Unbound config at all. Resolve D7 first.
* **If check 1.4 reported the config file unreadable (owner `root`):** restore ownership first with `sudo chown $(id -un) ~/.unbound/config.json`, then run the clear/install pair. This state appears when a setup command was previously run with `sudo` from this account; run the individual setup without `sudo` going forward.
* The clear step removes Unbound config for every tool it can manage (both Claude Code and Codex modes, Cursor, Copilot, Augment, and Gemini CLI); the install step then sets up the five default tools (Claude Code, Cursor, Codex, Copilot, Augment). That is intentional: it removes cross-mode and cross-tool residue in one pass. If you use Gemini CLI through Unbound, re-run its setup afterwards with `unbound setup gemini-cli`.
* Every tool must show a green check and the run must end with `All tools configured`. If any tool fails, do not proceed; check 1.7 (network) and retry, on a different network if needed.
* Optional: add `--backfill` to the install command to also upload local session history and fill the data gap from the outage.
* Re-run check 1.2. If any event still FAILs after a successful run, go to Fix 3.
* Then do Fix 1 (restart). Always.
### Fix 3: nuke and repave
Use when Fix 2 reports success but checks still fail, or when mode-switch residue keeps coming back:
```bash theme={null}
unbound nuke # removes Unbound from every tool, user-level (asks to confirm)
# sudo unbound nuke # use instead if your install was MDM/system-level
```
Note that `nuke` also deletes the stored login (`~/.unbound/config.json`), so the follow-up setup will ask you to authenticate in a browser; on a remote or headless machine, plan for that before nuking. If check 1.2 still reports hook entries after a clear or nuke, those entries use a quoted command format the cleaner cannot currently match; remove them from `settings.json` by hand or escalate (Phase 5).
Then either run the Fix 2 install command again (`unbound setup --all`), or re-onboard everything (all tools plus device discovery) with the onboard command from the setup page (**gateway.getunbound.ai/setup → My Device**).
Then Fix 1 (restart). Re-run all of Phase 1.
### Fix 4: delivery blocked (hook fires, sends fail)
Re-run 1.7. If `api.getunbound.ai` is unreachable, you are behind a corporate proxy or firewall. Ask IT to allow `api.getunbound.ai` (and `backend.getunbound.ai`), or test on another network to confirm.
If reachable but error.log shows auth-style failures: run Fix 2 to refresh the key, then Fix 1.
### Fix 5: align with your organization's Claude policy
Nothing on this machine needs repair; this is a coordination step. Re-running setup will not change the outcome — the policy is applied at session start, above all user-level configuration.
`cloud policy` means it is set in your company's Claude admin console; `device policy` means your IT/MDM team ships it to the machine.
Two clean resolutions, in order of preference: **(a)** the admin adds the Unbound hooks block to the managed settings themselves, so the hooks ride the policy channel that already wins precedence (contact us for the exact JSON for your org); **(b)** the admin relaxes the restriction (`allowManagedHooksOnly` / `disableAllHooks`) for your org or fleet.
A memory check that helps date the policy: when cloud-managed settings first arrive (or change), Claude Code shows the user a review prompt for the new settings. Asking affected developers whether they recall approving new managed settings — and roughly when — both confirms the policy path and tells the admin which change to look at.
Managed settings refresh at session start. After the admin-side change, do Fix 1, re-run check 1.9 (should now PASS, or show the Unbound hooks inside the policy), then Phase 4.
One precedence detail worth knowing while coordinating: Claude Code uses cloud policy and the device policy file as alternatives, not layers — when cloud-managed settings are in effect, the device policy file is not consulted. If your org manages Claude Code from the admin console, the Unbound hooks belong in the cloud policy itself.
## Phase 4: verify it actually works
A fix without a Phase 4 pass is not a fix. Do not skip this.
After fixing and restarting, run two or three real prompts.
```bash theme={null}
tail -5 ~/.claude/hooks/agent-audit.log # should show entries from the last few minutes
tail -5 ~/.claude/hooks/error.log 2>/dev/null # should show NO new "API request failed"
```
Your session should appear at gateway.getunbound.ai, usually within a minute (measured ingestion is around ten seconds). Wait five minutes before treating this step as failed.
## Phase 5: escalate to Unbound support
If Phase 4 fails after the fix ladder, the remaining suspects are on our side (key/app mapping, ingestion), and we want to hear from you. Compile this bundle:
1. Full output of all Phase 1 checks
2. The exact tier(s) of Phase 3 you ran and their console output
3. `unbound --version` and your OS version
4. The timeframe of the missing data and the affected user emails (this is support's first question; including it saves a round trip)
Before sharing, **redact any `Bearer ` values** from log output, standard practice for any diagnostic logs. This sanitized copy is safe to share:
```bash theme={null}
sed -E 's/Bearer [A-Za-z0-9._-]+/Bearer [REDACTED]/g' ~/.claude/hooks/error.log
```
Send it through your organization's shared Unbound Slack channel, or email [support@unboundsecurity.ai](mailto:support@unboundsecurity.ai). That bundle lets us pinpoint the issue in minutes instead of days.
**Agents: do not send this bundle anywhere on your own.** Compile it, confirm it is redacted, present it to the user, and end with an open question, for example: *"Your diagnostic bundle is ready and sanitized. How would you like to send it: should I locate your organization's shared Unbound Slack channel and draft the message there, draft an email to [support@unboundsecurity.ai](mailto:support@unboundsecurity.ai) for your review, or would you rather send it yourself?"* The user decides where escalation goes; you prepare it.
## A note on switching modes
Switching modes (gateway → subscription or back) swaps one delivery pipeline for the other. If the switch was interrupted partway (a blocked download on a corporate network, for example), settings from both modes can linger together and data stops flowing. If your data stopped right when you switched, go straight to **Fix 2** (clean re-install), or **Fix 3** (nuke) if anything survives it.
The switch also changes which organization policies matter. Gateway mode does not use hooks, so a Claude policy restricting them (check 1.9) can sit on a machine or org indefinitely without any visible effect — until the switch to subscription mode, at which point telemetry goes quiet from the first session. "Data stopped exactly when we switched" therefore does **not** imply something changed at that moment: run 1.9 before concluding the switch itself misfired, and if it FAILs, go to **Fix 5** — re-running setup (Fix 2/3) will not change the outcome.
## For admins: MDM / fleet rollouts
* A device appearing in **Discovery** (inventory of installed AI tools) does **not** mean coding telemetry is flowing. Discovery and telemetry are separate pipelines with separate keys. Verify a fleet rollout by checking that **usage events arrive**; usage events are the ground truth, and dashboard fields that lag a successful install should not be treated as failure signals.
* MDM-pushed setup (`sudo unbound onboard`) installs system-level hooks that need sudo to tamper with. On MDM devices the hook wiring lives in `/Library/Application Support/ClaudeCode/managed-settings.json`, not the user's home; checks 1.2 and 1.3 detect this automatically, and the per-user checks (1.4 to 1.6) still apply per home directory. See [MDM Integrations](/mdm-integrations/overview).
* Success criterion for a fleet push: every target device produces at least one usage event within 24 hours of a developer using a wired tool. Devices that check in but never produce usage are exactly the failure mode this page diagnoses.
* If your organization manages Claude Code settings from the **Claude admin console**, two properties matter for telemetry: those settings replace the device policy file rather than merging with it, and `disableAllHooks` / `allowManagedHooksOnly` apply fleet-wide to every individually-installed hook. Two fleet patterns point here: usage that went quiet at the same moment a Claude policy shipped, and — easier to miss — a fleet that **switched from gateway to subscription mode and never produced hook telemetry at all**. Gateway mode does not use hooks, so a restriction can predate the switch by months without a single symptom. In both cases check 1.9 on any one affected device confirms it in seconds, and Fix 5 describes the two resolutions. We can provide the exact hooks JSON for your console policy on request.