Kandji (Iru) Deployment Guide
Deploy Clawkeeper to your Mac fleet using Kandji. This covers Claude Code security hooks and Claude Cowork (Claude Desktop) MCP gateway policies — two custom scripts, one blueprint, full coverage with a single shared policy engine.
Overview
Clawkeeper deploys as two Kandji custom scripts:
- Claude Code hooks — installs the
clawkeeper-mcp-gatewaybinary (kept current on every run), drops~/.config/clawkeeper-mcp-gateway/config.json(user-owned, so the SessionStart hook can read it), and writes hooks to~/.claude/settings.json. HTTP hooks cover PreToolUse, PostToolUse, and UserPromptSubmit (Runtime Shield evaluation); atype: commandSessionStart hook callsclawkeeper-mcp-gateway scan-inventoryto capture installed skills, MCP servers, AND plugins from disk — including connectors that plugins bundle (Gmail, Slack, Google Calendar, etc. registered via a plugin's.mcp.jsonare inventoried automatically) - Cowork MCP gateway — re-uses the same gateway binary, runs
configure-ideto route Claude Desktop's MCP traffic through the gateway. Every MCP tool call the Cowork agent makes is evaluated against the sameclaude_code_policiesrow from the dashboard — path reads/writes, tool blocks, PII/PHI detection, bash-command blocks — so Cowork inherits Claude Code's policy enforcement without a second product setup
Both scripts run as root via the Kandji agent, detect the logged-in user, and write config to that user's home directory. Deploy one or both depending on which Claude products your team uses.
Requirements:
- Kandji (Iru) with custom script support
- Macs with Claude Code and/or Claude Desktop installed
- A Clawkeeper account (Team plan for fleet management)
- An API key from the Clawkeeper dashboard
Node.js is NOT required for either step. Step 2 (Claude Code hooks) is pure bash + python3 + the
clawkeeper-mcp-gatewayGo binary. Step 3 (Cowork MCP gateway) reuses that same binary — no Node.js, nonpx. Most customers start with Step 2 only and add Step 3 if their team uses Claude Desktop with Cowork.
Step 1: Get your API key
- Log in to clawkeeper.dev
- Go to Settings → API Keys
- Click Create Key — name it "Kandji Fleet" or similar
- Copy the key (starts with
ck_live_)
This key is write-only — it can send events and check policies but cannot read org data or modify settings.
Step 2: Deploy Claude Code hooks
This script installs (and on subsequent runs, auto-upgrades) the clawkeeper-mcp-gateway binary and writes hooks into ~/.claude/settings.json. Clawkeeper then evaluates every tool call + prompt submission in real time, and at the start of each session captures the full agent inventory: installed skills, MCP servers, and plugins — including the connectors plugins register via their .mcp.json manifests.
In Kandji, go to Library → Custom Scripts → Add New. Paste the following script:
#!/bin/bash
set -euo pipefail
# ============================================================
# Clawkeeper — Install Claude Code security hooks
# Runs as root. Writes config for the logged-in user.
# ============================================================
CLAWKEEPER_API_KEY="ck_live_YOUR_API_KEY_HERE"
CLAWKEEPER_API_URL="https://clawkeeper.dev/api/v1"
# Resolve logged-in user (Kandji scripts run as root)
CURRENT_USER=$(/usr/bin/stat -f%Su /dev/console)
if [[ "$CURRENT_USER" == "loginwindow" ]] || [[ "$CURRENT_USER" == "_mbsetupuser" ]] || [[ "$CURRENT_USER" == "root" ]]; then
echo "No regular user logged in (got: $CURRENT_USER), skipping"
exit 0
fi
USER_HOME=$(/usr/bin/dscl . -read /Users/"$CURRENT_USER" NFSHomeDirectory | awk '{print $2}')
if [[ -z "$USER_HOME" ]] || [[ ! -d "$USER_HOME" ]]; then
echo "Could not resolve home directory for $CURRENT_USER"
exit 1
fi
CLAUDE_DIR="${USER_HOME}/.claude"
SETTINGS_FILE="${CLAUDE_DIR}/settings.json"
GATEWAY_BIN="/usr/local/bin/clawkeeper-mcp-gateway"
GATEWAY_USER_CONFIG_DIR="${USER_HOME}/.config/clawkeeper-mcp-gateway"
GATEWAY_USER_CONFIG="${GATEWAY_USER_CONFIG_DIR}/config.json"
mkdir -p "$CLAUDE_DIR"
# ------------------------------------------------------------------
# Install / upgrade the clawkeeper-mcp-gateway binary
# (signed, cosign-verified via install-gateway.sh).
#
# The SessionStart hook calls `clawkeeper-mcp-gateway scan-inventory`
# to capture installed skills, MCP servers, and plugins from disk —
# something an HTTP-type hook cannot do, so this install unblocks the
# agent-inventory dashboard at clawkeeper.dev/claude-code/agent-inventory.
#
# --force + CLAWKEEPER_SKIP_SETUP=1 makes this idempotent: every Kandji
# run pulls the latest release. Pair this script with a Kandji daily
# execution frequency and the whole fleet tracks the current version
# automatically — important for a security tool where new detection
# logic and new inventory coverage land in gateway releases.
# ------------------------------------------------------------------
echo "Installing / upgrading clawkeeper-mcp-gateway..."
/usr/bin/curl -fsSL https://clawkeeper.dev/install-gateway.sh | \
CLAWKEEPER_SKIP_SETUP=1 bash -s -- --force
# Write the gateway config to the logged-in user's home directory.
# The SessionStart hook runs scan-inventory as that user (not root), so
# the config must be user-readable. Using ~/.config/ keeps the API key
# out of a world-readable /etc path and matches the gateway's native
# config-resolution order (XDG_CONFIG_HOME → $HOME/.config → /etc).
mkdir -p "$GATEWAY_USER_CONFIG_DIR"
python3 -c "
import json
with open('${GATEWAY_USER_CONFIG}', 'w') as f:
json.dump({'api_key': '${CLAWKEEPER_API_KEY}', 'api_url': 'https://clawkeeper.dev'}, f)
"
chmod 600 "$GATEWAY_USER_CONFIG"
# Backup existing settings
if [ -f "$SETTINGS_FILE" ]; then
cp "$SETTINGS_FILE" "${SETTINGS_FILE}.backup.$(date +%Y%m%d%H%M%S)"
fi
# Merge hooks into existing settings (preserves user customizations)
python3 -c "
import json, os
settings_path = '${SETTINGS_FILE}'
existing = {}
if os.path.exists(settings_path):
try:
with open(settings_path) as f:
existing = json.load(f)
except (json.JSONDecodeError, IOError):
existing = {}
api_key = '${CLAWKEEPER_API_KEY}'
api_url = '${CLAWKEEPER_API_URL}'
hooks = {
'UserPromptSubmit': [{
'matcher': '*',
'hooks': [{
'type': 'http',
'url': f'{api_url}/claude-code/evaluate',
'headers': {'Authorization': f'Bearer {api_key}'},
'timeout': 10
}]
}],
'PreToolUse': [{
'matcher': 'Bash|Edit|Write|Read|Glob|Grep|WebFetch|WebSearch',
'hooks': [{
'type': 'http',
'url': f'{api_url}/claude-code/evaluate',
'headers': {'Authorization': f'Bearer {api_key}'},
'timeout': 10
}]
}],
'PostToolUse': [{
'matcher': 'Bash|Edit|Write|Read|Glob|Grep|WebFetch|WebSearch',
'hooks': [{
'type': 'http',
'url': f'{api_url}/claude-code/audit',
'headers': {'Authorization': f'Bearer {api_key}'},
'timeout': 10
}]
}],
# SessionStart uses type:command so the gateway binary can scan the
# filesystem for installed skills + MCP servers — HTTP hooks only
# receive Claude Code's event envelope with no filesystem access.
'SessionStart': [{
'matcher': '*',
'hooks': [{
'type': 'command',
'command': '/usr/local/bin/clawkeeper-mcp-gateway scan-inventory',
'timeout': 30
}]
}]
}
existing['hooks'] = hooks
with open(settings_path, 'w') as f:
json.dump(existing, f, indent=2)
print('Hooks written successfully')
"
# Set ownership and permissions (600 because files contain the API key)
chown -R "$CURRENT_USER:staff" "$CLAUDE_DIR"
chown -R "$CURRENT_USER:staff" "$GATEWAY_USER_CONFIG_DIR"
chmod 600 "$SETTINGS_FILE"
chmod 600 "$GATEWAY_USER_CONFIG"
echo "Clawkeeper hooks + scan-inventory installed for $CURRENT_USER"
exit 0
Important: Replace
ck_live_YOUR_API_KEY_HEREwith your actual API key from Step 1.
Script settings:
- Name: Clawkeeper — Claude Code Hooks
- Execution frequency: Run daily (recommended) — the script is idempotent, auto-upgrades the gateway binary to the latest release on each run, and self-heals any drift on
~/.claude/settings.jsonor the gateway config file. Run once is acceptable only for pinned-version deployments - Show in Self Service: No
Step 3: Deploy Cowork MCP gateway (optional — policy parity with Claude Code)
Skip this step if you only use Claude Code. This step is for teams that also use Claude Desktop with Cowork. Reuses the
clawkeeper-mcp-gatewaybinary from Step 2 — no Node.js, no separate MCP server to install.
This script points Claude Desktop at the clawkeeper-mcp-gateway binary as a single MCP proxy entry. Every MCP tool call the Cowork agent makes — reading Google Drive files, writing Gmail drafts, running filesystem operations, invoking third-party MCP servers — is proxied through the gateway, evaluated against the same claude_code_policies row you already configured in the dashboard, and blocked / warned / allowed before reaching the upstream MCP server. Path read/write policies, blocked tools, blocked MCP skills, PII/PHI detection — Cowork inherits all of it. See CLA-58 policy parity for the full enforcement matrix.
In Kandji, go to Library → Custom Scripts → Add New. Paste the following script:
#!/bin/bash
set -euo pipefail
# ============================================================
# Clawkeeper — Wire Claude Desktop through the MCP gateway
# Runs as root. Reuses the gateway binary from Step 2; falls
# back to installing it if this is a Cowork-only deployment.
# ============================================================
CLAWKEEPER_API_KEY="ck_live_YOUR_API_KEY_HERE"
GATEWAY_BIN="/usr/local/bin/clawkeeper-mcp-gateway"
# Resolve logged-in user (Kandji scripts run as root)
CURRENT_USER=$(/usr/bin/stat -f%Su /dev/console)
if [[ "$CURRENT_USER" == "loginwindow" ]] || [[ "$CURRENT_USER" == "_mbsetupuser" ]] || [[ "$CURRENT_USER" == "root" ]]; then
echo "No regular user logged in (got: $CURRENT_USER), skipping"
exit 0
fi
USER_HOME=$(/usr/bin/dscl . -read /Users/"$CURRENT_USER" NFSHomeDirectory | awk '{print $2}')
if [[ -z "$USER_HOME" ]] || [[ ! -d "$USER_HOME" ]]; then
echo "Could not resolve home directory for $CURRENT_USER"
exit 1
fi
GATEWAY_USER_CONFIG_DIR="${USER_HOME}/.config/clawkeeper-mcp-gateway"
GATEWAY_USER_CONFIG="${GATEWAY_USER_CONFIG_DIR}/config.json"
# Install / upgrade the gateway binary. Idempotent — safe to run daily.
# If Step 2 already ran today, this is a no-op rebuild of the same version.
echo "Installing / upgrading clawkeeper-mcp-gateway..."
/usr/bin/curl -fsSL https://clawkeeper.dev/install-gateway.sh | \
CLAWKEEPER_SKIP_SETUP=1 bash -s -- --force
# Ensure the user-local config exists (Step 2 writes this; create if Cowork-only).
if [ ! -f "$GATEWAY_USER_CONFIG" ]; then
mkdir -p "$GATEWAY_USER_CONFIG_DIR"
python3 -c "
import json
with open('${GATEWAY_USER_CONFIG}', 'w') as f:
json.dump({'api_key': '${CLAWKEEPER_API_KEY}', 'api_url': 'https://clawkeeper.dev'}, f)
"
chown -R "$CURRENT_USER:staff" "$GATEWAY_USER_CONFIG_DIR"
chmod 600 "$GATEWAY_USER_CONFIG"
fi
# Invoke configure-ide as the logged-in user. This rewrites
# ~/Library/Application Support/Claude/claude_desktop_config.json so
# Claude Desktop routes ALL its MCP traffic through the gateway. Any
# MCP servers Cowork was already using (Google Drive, Gmail, custom
# MCPs) are migrated into the gateway's own servers[] list and routed
# through its policy engine transparently. Idempotent — safe to run
# daily for drift remediation.
/usr/bin/sudo -u "$CURRENT_USER" "$GATEWAY_BIN" configure-ide
echo "Clawkeeper Cowork MCP gateway wired for $CURRENT_USER"
exit 0
Important: Replace
ck_live_YOUR_API_KEY_HEREwith the same API key from Step 1.
Script settings:
- Name: Clawkeeper — Cowork MCP Gateway
- Execution frequency: Kandji's Custom Script picker offers Run once per device / Run every 15 minutes / Run daily / Run on-demand from Self Service. Pick Run daily —
configure-ideis idempotent, so daily re-runs are free and self-heal any user or MDM-induced drift. Pick Run every 15 minutes instead only if your fleet sees frequent config tampering and you want fast remediation. - Show in Self Service: No
Step 4: Assign to a Blueprint
- In Kandji, go to Library and find both scripts
- Click each script → Blueprints tab
- Assign to the Blueprint containing your target Macs
Scripts execute on the next Kandji agent check-in (typically within 15 minutes).
Step 5: Verify deployment
After the scripts run, verify on a test machine:
Claude Code:
# 1. settings.json exists with correct permissions
ls -la ~/.claude/settings.json
# Expected: -rw------- youruser staff
# 2. Gateway binary installed and executable
ls -la /usr/local/bin/clawkeeper-mcp-gateway
# Expected: -rwxr-xr-x root wheel (any non-zero size)
# 3. Gateway config is valid JSON with the correct api_url
python3 -c "
import json, os
c = json.load(open(os.path.expanduser('~/.config/clawkeeper-mcp-gateway/config.json')))
assert c.get('api_url','').startswith('https://clawkeeper.dev'), 'bad api_url: %r' % c.get('api_url')
assert c.get('api_key','').startswith('ck_live_'), 'bad api_key prefix'
print('config.json: OK')
"
# Validates user-readable config path (no sudo needed — that's the point).
# 4. Hooks are present
python3 -c "
import json
s = json.load(open('$HOME/.claude/settings.json'))
print('Hooks:', list(s.get('hooks',{}).keys()))
"
# Expected: Hooks: ['UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'SessionStart']
# 5. scan-inventory runs end-to-end and returns a non-empty payload.
# The `< /dev/null` is required when running manually from a terminal:
# scan-inventory reads its session envelope from stdin, and an interactive
# TTY never EOFs, so without the redirect the command would hang.
/usr/local/bin/clawkeeper-mcp-gateway scan-inventory --dry-run < /dev/null \
| python3 -c "import json,sys;d=json.load(sys.stdin);print('skills:',len(d.get('installed_skills',[])),'mcp:',len(d.get('installed_mcp_servers',[])),'plugins:',len(d.get('installed_plugins',[])))"
# Expected: non-zero counts on any developer workstation with skills/MCP/plugins installed.
# If all three are 0 on a known-configured Mac, the scan is misfiring — see Troubleshooting.
# If plugins: 0 but the machine has Claude Code plugins installed, verify the gateway
# is on v0.6.0 or later — older releases don't populate `installed_plugins`.
In Claude Code, run /hooks — you should see 3 Clawkeeper HTTP hooks (UserPromptSubmit, PreToolUse, PostToolUse) plus a type: command SessionStart hook calling clawkeeper-mcp-gateway scan-inventory.
Cowork:
# Check Claude Desktop config points at the gateway binary
python3 -c "
import json
c = json.load(open('$HOME/Library/Application Support/Claude/claude_desktop_config.json'))
for name, cfg in c.get('mcpServers', {}).items():
print(name, '->', cfg.get('command', ''), ' '.join(cfg.get('args', [])))
"
# Expected: one entry whose command is /usr/local/bin/clawkeeper-mcp-gateway
# Spot-check the policy round-trip:
/usr/local/bin/clawkeeper-mcp-gateway scan-inventory --dry-run < /dev/null | python3 -c "import json,sys;d=json.load(sys.stdin);print('skills:',len(d['installed_skills']),'mcp:',len(d['installed_mcp_servers']),'plugins:',len(d.get('installed_plugins',[])))"
Restart Claude Desktop. All MCP tool calls Cowork makes now proxy through the gateway and are evaluated server-side against your dashboard policies. Blocked calls return a deny verdict that Cowork surfaces in its conversation; warned calls proceed with an advisory flag. Events show up at clawkeeper.dev/mcp-gateway.
Dashboard: Open clawkeeper.dev/claude-code/agent-inventory — workstations appear as users start sessions, along with the skills, MCP servers, and plugins installed on each.
Drift remediation (optional)
To ensure configs aren't removed by users, use Kandji's audit/remediation model. Set execution frequency to Run daily, or create a separate audit script:
#!/bin/bash
# Audit: exits 0 if configs are correct, 1 if missing (triggers remediation)
CURRENT_USER=$(/usr/bin/stat -f%Su /dev/console)
if [[ "$CURRENT_USER" == "loginwindow" ]] || [[ "$CURRENT_USER" == "_mbsetupuser" ]] || [[ "$CURRENT_USER" == "root" ]]; then
exit 0 # No user = don't trigger remediation
fi
USER_HOME=$(/usr/bin/dscl . -read /Users/"$CURRENT_USER" NFSHomeDirectory | awk '{print $2}')
if [[ -z "$USER_HOME" ]] || [[ ! -d "$USER_HOME" ]]; then
exit 0
fi
FAILED=0
# Check Claude Code hooks
if ! python3 -c "
import json
s = json.load(open('${USER_HOME}/.claude/settings.json'))
assert 'hooks' in s
assert 'PreToolUse' in s['hooks']
assert 'clawkeeper.dev' in json.dumps(s['hooks'])
" 2>/dev/null; then
echo "FAIL: Claude Code hooks missing"
FAILED=1
fi
# Check Cowork MCP gateway (configure-ide's signature: a single
# mcpServers entry whose command is clawkeeper-mcp-gateway + "server")
if ! python3 -c "
import json
c = json.load(open('${USER_HOME}/Library/Application Support/Claude/claude_desktop_config.json'))
servers = c.get('mcpServers', {})
found = any(
isinstance(cfg, dict) and 'clawkeeper-mcp-gateway' in (cfg.get('command') or '')
for cfg in servers.values()
)
assert found, 'clawkeeper-mcp-gateway not wired into mcpServers'
" 2>/dev/null; then
echo "FAIL: Cowork MCP gateway not wired"
FAILED=1
fi
if [[ $FAILED -eq 1 ]]; then
exit 1 # Triggers remediation
fi
echo "Clawkeeper configs verified"
exit 0
Use the install scripts from Steps 2-3 as the remediation scripts.
Shared Macs
Scripts detect the console user at execution time. On shared Macs with multiple accounts:
- Only the user logged in when the Kandji agent runs gets the config
- Set execution frequency to Run daily so each user gets the config on their next login
- Other users get configured on their next session
Updating
- Evaluation logic: No update needed. HTTP hooks call
clawkeeper.dev/api/v1, which always serves the latest detection + policy logic server-side. - Gateway binary (
clawkeeper-mcp-gateway): Auto-upgrades on every Kandji run. The Step 2 and Step 3 scripts invokeinstall-gateway.shwith--force+CLAWKEEPER_SKIP_SETUP=1unconditionally, so pairing this with a Kandji Run daily frequency keeps the fleet on the current release. New detection logic, new inventory coverage, and security fixes ship in releases — an out-of-date binary means missed visibility. The install path is cosign-verified against a signed checksums file, so auto-upgrade is safe. Pin a specific version in-house by mirroring the tarball and pointing the install script'sCLAWKEEPER_GATEWAY_VERSIONenv var at it (see MCP Gateway enterprise install). - Config file (
~/.config/clawkeeper-mcp-gateway/config.json): Re-run the Kandji script after rotating the API key. The file is rewritten idempotently with the current value each run.
Removing Clawkeeper from a fleet
Use the public uninstaller for full cleanup. It removes Claude Code hooks, plugin cache entries, state directories, the clawkeeper-mcp-gateway binary, user gateway config, and Claude Desktop MCP gateway registrations. If configure-ide created an *.agentkeeper-backup-* file, the uninstaller restores the latest backup; otherwise it prunes only Clawkeeper/gateway MCP entries and preserves unrelated MCP servers.
Pilot with dry run first:
curl -fsSL https://clawkeeper.dev/uninstall.sh | bash -s -- --dry-run
Then run live cleanup through Kandji:
#!/bin/bash
CONSOLE_USER=$(stat -f %Su /dev/console)
su - "$CONSOLE_USER" -c "curl -fsSL https://clawkeeper.dev/uninstall.sh | bash -s -- --quiet"
If your Kandji item must run entirely as root, pass TARGET_USER=<shortname> or resolve the console user as shown above so the script cleans the real user's home directory instead of root's.
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| Script shows FAIL in Kandji | Script error | Check device logs in Kandji console → device → Activity |
| Script exits 0 but no config | No user logged in | Set to Run daily — catches the user on next check-in |
| Hooks not loading in Claude Code | File permissions or ownership | Verify: ls -la ~/.claude/settings.json — should be owner rw, mode 600 |
/usr/local/bin/clawkeeper-mcp-gateway missing | install-gateway.sh failed (network, TLS interception, cosign verification) | Re-run /usr/bin/curl -fsSL https://clawkeeper.dev/install-gateway.sh | bash manually on the device and read the output. Corporate proxies that strip code-signing headers will fail cosign verification |
| SessionStart hook runs but dashboard shows 0 skills / 0 MCP servers | scan-inventory can't read the target user's ~/.claude tree, or config JSON is invalid | As the logged-in user: /usr/local/bin/clawkeeper-mcp-gateway scan-inventory --dry-run < /dev/null. The stdin redirect is required — scan-inventory reads its session envelope from stdin and a terminal TTY never EOFs. If the command errors on config parse, compare ~/.config/clawkeeper-mcp-gateway/config.json against the expected shape |
scan-inventory exits non-zero in session logs | Invalid ~/.config/clawkeeper-mcp-gateway/config.json | python3 -m json.tool < ~/.config/clawkeeper-mcp-gateway/config.json to validate. Re-run the Kandji script to rewrite the file |
scan-inventory hangs when run manually from a terminal | Older gateway (≤ v0.5.0) blocks on a stdin read for the session envelope; the TTY never produces EOF | Upgrade to v0.5.1+, or work around by redirecting stdin: scan-inventory ... < /dev/null |
| Skills + MCP show up, but Plugins section is empty | Gateway is older than v0.6.0 (no installed_plugins payload) | Verify with /usr/local/bin/clawkeeper-mcp-gateway version. If < v0.6.0, re-run the Kandji script — the install step now runs --force on every execution and will pull the latest release. Scan output should show plugins: N with N > 0 on any Mac with installed plugins |
| Plugin counts correct in scan output but absent from dashboard | Supabase migration host_plugin_inventory not applied yet (brief window right after a deploy) | Wait 2-3 minutes and retry. The checkin endpoint is fail-open: skill + MCP rows persist correctly regardless; only plugin rows are affected until the table exists |
| MCP gateway not wired into Cowork | Claude Desktop not restarted | Quit and reopen Claude Desktop after Step 3 runs |
| Events not in dashboard | Wrong API key | Verify key at clawkeeper.dev/settings — should start with ck_live_ |
| python3 not found | Xcode CLT not installed | Deploy Xcode Command Line Tools via Kandji |
Security considerations
- The API key is write-only — it can send events and fetch policies, but cannot read data or modify settings.
- Config files are set to mode 600 (owner read/write only) because they contain the API key.
- No file contents are sent to Clawkeeper — only file paths, hostnames, tool names, and detection metadata.
- Hook timeout is 10 seconds. If
clawkeeper.devis unreachable, Claude Code continues normally (fail-open). - Cowork MCP tools run locally in under 10ms. Dashboard reporting is async and non-blocking.
- Scripts run as root but
chownall files back to the logged-in user before exiting.
