> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getunbound.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

<Tip>
  **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, refreshes the local policy cache the way any prompt does, and sends one labeled synthetic telemetry event.
</Tip>

## Background: how usage data flows

Unbound captures Claude Code usage in one of two modes:

| 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.

Four 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.* The hook keeps the last 25 errors in `~/.claude/hooks/error.log`, so check 1.6 is where a delivery problem shows up. 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 a blocked call is told *"The organization policy engine could not be reached."*
2. **Restart Claude Code after every fix, and prove the restart took.** Fully quit it, relaunch from a new terminal, send one prompt, then re-run check 1.6: an `agent-audit.log` entry timestamped after the relaunch is the evidence that the configuration now on disk is the one being used. Until you see that entry, treat the fix as unverified.
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.3 below do by hand. It does not read the local hook logs, so check 1.6 is still yours to run.
4. **Your company may manage Claude Code centrally as well.** Settings can reach the machine from your company's Claude admin console or as a device policy file from IT, in files outside your home directory. When one of those carries a hook restriction, an install that passes every check above still produces nothing, and it produces it silently: `agent-audit.log` stays empty and `error.log` gains no entry. Check 1.9 reads those files and tells you 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:
    value = json.load(open(os.path.expanduser('~/.claude/settings.json'))).get('apiKeyHelper')
    # Only Unbound's own helper means gateway mode. The subscription install
    # deliberately leaves a helper your org wrote in place, so a bare
    # "apiKeyHelper is set" test would misread that machine as gateway.
    helper = isinstance(value, str) and value.strip().endswith('anthropic_key.sh')
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.

<Warning>
  **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.
</Warning>

### 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')))
    helper = user.get('apiKeyHelper')
    # Unbound's gateway helper is the one at <config dir>/anthropic_key.sh. A helper
    # your organization wrote is not residue — the subscription install leaves it alone.
    ours = isinstance(helper, str) and helper.strip().endswith('anthropic_key.sh')
    print(f"{'FAIL' if ours else 'PASS'} no Unbound gateway apiKeyHelper (must be absent in subscription mode)")
except Exception:
    print('PASS no Unbound gateway apiKeyHelper (no user settings.json)')
EOF
```

Expected in subscription mode: an install locus line, all six events PASS, and the apiKeyHelper line PASS. Only Unbound's own gateway helper counts as residue here — a key helper your organization wrote is left in place on purpose, so it is not a finding. 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 "${PROGRAMFILES:-/nonexistent}/ClaudeCode/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 paths to be absent — one per platform, matching the managed directories check 1.2 scans. 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: <set>" || echo "env: <unset>"
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:-<unset>}"
[ -n "$UNBOUND_API_KEY" ] && echo "UNBOUND_API_KEY: <set>" || echo "UNBOUND_API_KEY: <unset>"
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/=.*/=<redacted>/' | 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
```

<Warning>
  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 <key>` values first.
</Warning>

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 `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 -L --max-time 15 -o /dev/null -w "setup download host: HTTP %{http_code}\n" https://raw.githubusercontent.com/websentry-ai/setup/refs/heads/main/claude-code/hooks/setup.py
```

Expected: both return 200 (reachable). Timeouts, `000`, or TLS errors mean a network/proxy block.

The second URL is the real one: `unbound setup` and `unbound nuke` fetch each tool's installer from `raw.githubusercontent.com` and run it locally, so that host has to be reachable for any of Phase 3 to work. (`getunbound.ai/setup/...` is a different channel — the curl-piped installer admins paste into MDM, covered in the admin section at the end.)

### 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]`; 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" ] || HOOK="${PROGRAMFILES:-/nonexistent}/ClaudeCode/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)

Two files on the machine can carry settings your organization set rather than you: `~/.claude/remote-settings.json`, where settings from your company's Claude admin console land, and a **device policy** file installed by IT/MDM at the platform path. The block below reads both and reports whether either carries a `disableAllHooks` or an `allowManagedHooksOnly` entry. Those are the two entries a hook restriction is written with, and if one is there the next move is with whoever set it (Fix 5), not with this machine. Reading one of these files by hand, look in two places: `allowManagedHooksOnly` lives inside the `permissions` object, `disableAllHooks` at the top level.

```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
    # allowManagedHooksOnly is nested under "permissions"; disableAllHooks sits at the
    # top level. Read both places for both keys — a top-level-only test would report a
    # real restriction as restrictions=None, and PASS a machine that is carrying one.
    perms = s.get('permissions') if isinstance(s.get('permissions'), dict) else {}
    keys = {k: (s[k] if k in s else perms[k])
            for k in ('disableAllHooks', 'allowManagedHooksOnly') if k in s or k in perms}
    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:
        line += ' (a non-empty cloud policy file is present too)'
    print(line)
    if keys.get('disableAllHooks') or (keys.get('allowManagedHooksOnly') and not wired):
        flagged = True
print('FAIL an organization policy on this machine carries a hook restriction (decision row D2)' if flagged else 'PASS no organization policy here restricts hooks')
EOF
```

Expected: `PASS`. A `FAIL` here is the explanation for a machine where everything else passes — including 1.8, where you run the hook script directly, so it passes whether or not the coding tool is running it. The decision table still applies top-down: if D1 also matched (broken wiring *and* a restriction present), repair the wiring with Fix 2 first, then re-run Phase 1 — this row will still be here.

Reading the output: the presence of a file is not the signal, the restriction keys are. A file reported `present` with `restrictions=None` carries neither key and is not a finding — an empty or placeholder policy file is a normal thing to have on a machine. If `~/.claude/remote-settings.json` is reported `absent`, do not conclude anything yet: start one Claude Code session, send a prompt, quit, and re-run this check. If it is still absent after that, there is no cloud policy file here for D2 to match on.

Two cross-checks, both for the human at the keyboard:

* In an interactive Claude Code session, run `/status` and read out the configuration sources it lists. Slash commands are not available to shell-driven agents (`claude -p "/status"` reports them unavailable) — an agent should ask the human for this rather than attempt it.
* Ask which Claude account and organization this machine is signed into. That is the admin console Fix 5 has you contact, and it is worth establishing before anyone goes looking 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 | An organization policy on this machine carries a hook restriction. The Unbound install itself is healthy, so expect 1.8 to pass (you ran the hook) while 1.6 shows no recent audit entries (Claude did not) | **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 `raw.githubusercontent.com` |
| 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 coding tool is not running the hook — restart it (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.

<Note>
  **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.
</Note>

### Fix 1: restart properly

<Note>
  **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".
</Note>

<Steps>
  <Step title="Fully quit Claude Code">
    Close every session, tab, and window, not just the conversation.
  </Step>

  <Step title="Open a brand-new terminal">
    So refreshed environment variables load.
  </Step>

  <Step title="Relaunch Claude Code from that terminal, then send one prompt">
    Now check `~/.claude/hooks/agent-audit.log` for an entry timestamped after the relaunch. That entry is the restart working; no entry means it did not.
  </Step>
</Steps>

### 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`.** Both commands download each tool's installer before doing anything, so on a blocked network the clear is a no-op that removes nothing — but a network that drops between the two commands leaves the machine cleared and not reinstalled. Resolve D7 first, and run the pair back to back.
* **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, Pi, 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 or Pi through Unbound, re-run their setup afterwards with `unbound setup gemini-cli` / `unbound setup pi`.
* 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, they name a path the cleaner was not looking at — typically a stale `~/.claude` path left behind after `CLAUDE_CONFIG_DIR` changed. Remove those 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 **Configure** page (**gateway.getunbound.ai/configure → My device**).

Then Fix 1 (restart). Re-run all of Phase 1.

### Fix 4: delivery blocked (hook fires, sends fail)

<Steps>
  <Step title="Re-check network reachability">
    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.
  </Step>

  <Step title="Refresh credentials">
    If reachable but error.log shows auth-style failures: run Fix 2 to refresh the key, then Fix 1.
  </Step>
</Steps>

### Fix 5: align with your organization's Claude policy

Nothing on this machine needs repair; this is a coordination step. Checks 1.2 to 1.8 already passed, so re-running setup would rewrite configuration that is already correct and leave check 1.6 exactly as empty as it is now.

<Steps>
  <Step title="Identify the policy source from check 1.9">
    `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.
  </Step>

  <Step title="Loop in the owning admin">
    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.

    To date the policy for the admin, use the two clocks on the machine: `ls -l` the file check 1.9 found, and take the last entry in `~/.claude/hooks/agent-audit.log` before the data went quiet. A policy file written around the time the audit log stops is the change to ask about.
  </Step>

  <Step title="Restart and verify">
    After the admin-side change, do Fix 1, re-run check 1.9 — it should now PASS, or show the Unbound hooks inside the policy — then Phase 4.
  </Step>
</Steps>

One thing worth settling with the admin early: which of the two channels their organization actually uses. If they manage Claude Code from the admin console, put the Unbound hooks in that policy rather than in a device file, so there is one place that carries them and one place to check.

## Phase 4: verify it actually works

<Warning>
  A fix without a Phase 4 pass is not a fix. Do not skip this.
</Warning>

<Steps>
  <Step title="Start a NEW Claude Code session">
    After fixing and restarting, run two or three real prompts.
  </Step>

  <Step title="Check the local evidence">
    ```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"
    ```
  </Step>

  <Step title="Check the Unbound dashboard">
    Your session should appear at gateway.getunbound.ai, usually within a minute. Wait five minutes before treating this step as failed. Check **Logs** for this, not the analytics breakdowns — those can lag hours behind Logs without anything being wrong.
  </Step>
</Steps>

## Phase 5: escalate to Unbound support

If Phase 4 fails after the fix ladder, every check this page can make has passed on the machine — 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)

<Warning>
  Before sharing, **redact any `Bearer <key>` 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
  ```
</Warning>

Email it to [support@unboundsecurity.ai](mailto:support@unboundsecurity.ai).

<Note>
  **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 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.
</Note>

## 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 governance are installed separately. 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**, a `disableAllHooks` or `allowManagedHooksOnly` entry in that policy reaches every device the policy covers, so one console change can take a whole fleet's hook telemetry out at once. 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.