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

# Deploy hooks for Claude Code

> Install Endor Labs hooks for Claude Code on macOS, Linux, and Windows developer machines to capture session, prompt, and tool events.

export const agent_3 = "Claude Code"

export const actionTerm_2 = "tool call"

export const agent_2 = "Claude Code"

export const actionTerm_1 = "tool call"

export const agent_1 = "Claude Code"

export const agent_0 = "Claude Code"

export const actionTerm_0 = "tool call"

export const YamlTable = ({children, data: propData, content}) => {
  const KV_RE = /^([A-Za-z][A-Za-z0-9_()/#\s-]+?):\s*(.+)$/;
  const INLINE_MD_RE = /(\[([^\]]+)\]\(([^)]+)\))|(`([^`]+)`)|(\*\*([^*]+)\*\*)|(\*([^*]+)\*)/g;
  const YES_RE = /^-yes-$/i;
  const NO_RE = /^-no-$/i;
  const LIMITED_RE = /^-(limited|partial)-$/i;
  const NA_RE = /^-(na|none)-$/i;
  const NA2_RE = /^-na2-$/i;
  const SIMPLE_TAG_RE = /(<br\s*\/?>)|(<p\s*\/?>)|(-note-)|(-warning-)/gi;
  const renderSuccessIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z" fill="currentColor" />
    </svg>;
  const renderFailureIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path fillRule="evenodd" clipRule="evenodd" d="M11.9902 1.98633C12.5208 1.9864 13.0426 2.12743 13.501 2.39453C13.9581 2.66107 14.3372 3.04388 14.5986 3.50391L22.5967 17.5L22.6895 17.6738C22.8921 18.0851 22.9979 18.5387 22.9981 18.999C22.9981 19.5253 22.8605 20.0431 22.5977 20.499C22.3348 20.9549 21.9555 21.3332 21.5 21.5967C21.0445 21.8601 20.5272 21.9994 20.001 22H4.00001C3.47453 22.0031 2.95699 21.868 2.50001 21.6084C2.04032 21.3471 1.65712 20.9684 1.39063 20.5117C1.12431 20.055 0.983643 19.5355 0.982429 19.0068C0.981281 18.4793 1.11967 17.9611 1.38282 17.5039L9.38184 3.50391C9.64344 3.04359 10.023 2.66111 10.4805 2.39453C10.9388 2.12755 11.4598 1.98636 11.9902 1.98633ZM12 16.9004C11.3925 16.9004 10.9004 17.3925 10.9004 18C10.9004 18.6075 11.3925 19.0996 12 19.0996H12.0098C12.6173 19.0996 13.1104 18.6075 13.1104 18C13.1104 17.3925 12.6173 16.9004 12.0098 16.9004H12ZM12 5.90039C11.3925 5.9004 10.9004 6.39249 10.9004 7V13C10.9004 13.6075 11.3925 14.0996 12 14.0996C12.6075 14.0996 13.0996 13.6075 13.0996 13V7C13.0996 6.39249 12.6075 5.90039 12 5.90039Z" fill="currentColor" />
    </svg>;
  const renderPartialSuccessIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path d="M12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1ZM12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM12 5C13.8565 5 15.6374 5.73705 16.9502 7.0498C18.263 8.36256 19 10.1435 19 12C19 13.8565 18.263 15.6374 16.9502 16.9502C15.6374 18.263 13.8565 19 12 19C11.4477 19 11 18.5523 11 18V6C11 5.73478 11.1054 5.4805 11.293 5.29297C11.4805 5.10543 11.7348 5 12 5Z" fill="currentColor" />
    </svg>;
  const renderPendingIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
      <path d="M10.1 2.182a10 10 0 0 1 3.8 0" />
      <path d="M13.9 21.818a10 10 0 0 1-3.8 0" />
      <path d="M17.609 3.721a10 10 0 0 1 2.69 2.7" />
      <path d="M2.182 13.9a10 10 0 0 1 0-3.8" />
      <path d="M20.279 17.609a10 10 0 0 1-2.7 2.69" />
      <path d="M21.818 10.1a10 10 0 0 1 0 3.8" />
      <path d="M3.721 6.391a10 10 0 0 1 2.7-2.69" />
      <path d="M6.391 20.279a10 10 0 0 1-2.69-2.7" />
    </svg>;
  const renderRunningIcon = () => <svg className="yt-status-running-svg" viewBox="22 22 44 44" width="100%" height="100%" aria-hidden="true">
      <circle className="yt-status-running-circle" cx="44" cy="44" r="20.2" fill="none" stroke="currentColor" strokeWidth="3.6" />
    </svg>;
  const renderSkippedIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <circle cx="12" cy="12" r="10" />
      <path d="M8 12h8" />
    </svg>;
  const STATUS_ICON_DEFS = [{
    re: /^-status-success-$/i,
    colorClass: 'yt-status-color-success',
    label: 'Success',
    render: renderSuccessIcon
  }, {
    re: /^-status-failure-$/i,
    colorClass: 'yt-status-color-failure',
    label: 'Failure',
    render: renderFailureIcon
  }, {
    re: /^-status-partial-success-$/i,
    colorClass: 'yt-status-color-partial',
    label: 'Partial success',
    render: renderPartialSuccessIcon
  }, {
    re: /^-status-pending-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Pending',
    render: renderPendingIcon
  }, {
    re: /^-status-running-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Running',
    render: renderRunningIcon
  }, {
    re: /^-status-skipped-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Skipped',
    render: renderSkippedIcon
  }];
  const renderStatusBadge = def => <span className={['yt-status-icon', def.colorClass].join(' ')} role="img" aria-label={def.label} title={def.label}>
      {def.render()}
    </span>;
  const tryParseKV = trimmed => {
    const m = KV_RE.exec(trimmed);
    return m ? {
      key: m[1],
      value: m[2].trim()
    } : null;
  };
  const registerKey = (key, seenKeys, orderedKeys) => {
    if (!seenKeys.has(key)) {
      orderedKeys.push(key);
      seenKeys.add(key);
    }
  };
  const flushEntry = (currentEntry, entries) => {
    if (Object.keys(currentEntry).length > 0) entries.push(currentEntry);
  };
  const parseDashPrefixed = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed.startsWith('- ')) {
        if (inEntry) entries.push(currentEntry);
        currentEntry = {};
        inEntry = true;
        const kv = tryParseKV(trimmed.substring(2).trim());
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      } else if (inEntry && trimmed !== '') {
        const kv = tryParseKV(trimmed);
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      }
    }
    flushEntry(currentEntry, entries);
  };
  const parseBlankSeparated = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed === '') {
        if (inEntry) {
          flushEntry(currentEntry, entries);
          currentEntry = {};
          inEntry = false;
        }
        continue;
      }
      const kv = tryParseKV(trimmed);
      if (!kv) continue;
      const isNewEntry = !line.startsWith(' ') && !line.startsWith('\t');
      if (isNewEntry && inEntry && Object.keys(currentEntry).length > 0) {
        entries.push(currentEntry);
        currentEntry = {};
      }
      registerKey(kv.key, seenKeys, orderedKeys);
      currentEntry[kv.key] = kv.value;
      inEntry = true;
    }
    flushEntry(currentEntry, entries);
  };
  const normalizeEntries = (entries, orderedKeys) => entries.map(entry => {
    const filled = {};
    for (const key of orderedKeys) filled[key] = entry[key] || '';
    return filled;
  });
  const parseYamlTableContent = contentStr => {
    if (!contentStr) return [];
    const entries = [];
    const orderedKeys = [];
    const seenKeys = new Set();
    const lines = contentStr.split('\n');
    if (lines.some(line => line.trim().startsWith('- '))) {
      parseDashPrefixed(lines, entries, orderedKeys, seenKeys);
    } else {
      parseBlankSeparated(lines, entries, orderedKeys, seenKeys);
    }
    return normalizeEntries(entries, orderedKeys);
  };
  const processText = text => {
    if (!text) return text;
    const parts = [];
    let keyIndex = 0;
    let lastIndex = 0;
    let match;
    while ((match = INLINE_MD_RE.exec(text)) !== null) {
      if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
      if (match[1]) {
        parts.push(<a key={keyIndex++} href={match[3]}>{match[2]}</a>);
      } else if (match[4]) {
        parts.push(<code key={keyIndex++}>{match[5]}</code>);
      } else if (match[6]) {
        parts.push(<strong key={keyIndex++}>{match[7]}</strong>);
      } else if (match[8]) {
        parts.push(<em key={keyIndex++}>{match[9]}</em>);
      }
      lastIndex = match.index + match[0].length;
    }
    if (lastIndex < text.length) parts.push(text.slice(lastIndex));
    if (parts.length === 0) return text;
    const keyRef = {
      current: keyIndex
    };
    return expandHtmlTags(parts, keyRef);
  };
  const processBadges = text => {
    if (!text || typeof text !== 'string') return text;
    if (YES_RE.test(text)) return <span className="yt-badge-yes" role="img" aria-label="Supported" title="Supported">✓</span>;
    if (NO_RE.test(text)) return <span className="yt-badge-no" role="img" aria-label="Not supported" title="Not supported">✗</span>;
    if (LIMITED_RE.test(text)) return <span className="yt-badge-limited" role="img" aria-label="Partially supported" title="Partially supported">◐</span>;
    if (NA_RE.test(text) || NA2_RE.test(text)) return <span className="yt-sr-only" title="Not applicable">Not applicable</span>;
    const statusBadge = STATUS_ICON_DEFS.find(def => def.re.test(text));
    if (statusBadge) return renderStatusBadge(statusBadge);
    return processText(text);
  };
  const cellClassName = text => {
    if (!text || typeof text !== 'string') return undefined;
    if (NA_RE.test(text)) return 'yt-cell-na';
    if (NA2_RE.test(text)) return 'yt-cell-na2';
    return undefined;
  };
  const expandSimpleTags = (str, keyRef) => {
    const result = [];
    let last = 0;
    SIMPLE_TAG_RE.lastIndex = 0;
    let m;
    while ((m = SIMPLE_TAG_RE.exec(str)) !== null) {
      if (m.index > last) result.push(str.slice(last, m.index));
      if (m[1]) {
        result.push(<br key={keyRef.current++} />);
      } else if (m[2]) {
        result.push(<br key={keyRef.current++} />, <br key={keyRef.current++} />);
      } else if (m[3]) {
        result.push(<span key={keyRef.current++} className="yt-badge-note" style={{
          fontWeight: 600
        }}>Note: </span>);
      } else if (m[4]) {
        result.push(<span key={keyRef.current++} className="yt-badge-warning" style={{
          fontWeight: 600
        }}>Warning: </span>);
      }
      last = m.index + m[0].length;
    }
    if (last < str.length) result.push(str.slice(last));
    return result;
  };
  const expandHtmlTags = (chunks, keyRef) => {
    const out = [];
    for (const chunk of chunks) {
      if (typeof chunk === 'string') {
        out.push(...expandSimpleTags(chunk, keyRef));
      } else {
        out.push(chunk);
      }
    }
    return out;
  };
  const extractText = node => {
    if (node === null || node === undefined) return '';
    if (typeof node === 'string') return node;
    if (typeof node === 'number') return String(node);
    if (typeof node === 'boolean') return '';
    if (Array.isArray(node)) return node.map(extractText).join('');
    if (node && typeof node === 'object' && node.type) {
      const props = node.props || ({});
      if (typeof props.children === 'string') return props.children;
      if (props.children) return extractText(props.children);
      return '';
    }
    return String(node || '');
  };
  const [mounted, setMounted] = useState(false);
  const scrollWrapRef = useRef(null);
  const [isScrollable, setIsScrollable] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  const data = useMemo(() => {
    if (propData) return propData;
    if (content && typeof content === 'string') return parseYamlTableContent(content);
    if (!children) return [];
    if (typeof children === 'string') return parseYamlTableContent(children);
    const childrenArray = Array.isArray(children) ? children : [children];
    return parseYamlTableContent(childrenArray.map(extractText).join('').trim());
  }, [children, propData, content]);
  const columns = useMemo(() => {
    if (!data || data.length === 0) return [];
    const firstRow = data[0];
    if (!firstRow || typeof firstRow !== 'object') return [];
    return Object.keys(firstRow);
  }, [data]);
  useEffect(() => {
    const wrap = scrollWrapRef.current;
    if (!wrap) return undefined;
    const updateScrollable = () => {
      setIsScrollable(wrap.scrollWidth > wrap.clientWidth);
    };
    updateScrollable();
    const observer = new ResizeObserver(updateScrollable);
    observer.observe(wrap);
    return () => observer.disconnect();
  }, [mounted, data]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <div ref={scrollWrapRef} style={{
    overflowX: 'auto',
    margin: '1.75rem 0'
  }} role={isScrollable ? 'region' : undefined} aria-label={isScrollable ? 'Scrollable table' : undefined} tabIndex={isScrollable ? 0 : undefined}>
      <table style={{
    display: 'table',
    width: '100%',
    minWidth: '100%',
    margin: 0
  }}>
        <thead>
          <tr>
            {columns.map(col => <th key={col}>{col.replaceAll('_', ' ')}</th>)}
          </tr>
        </thead>
        <tbody>
          {data.map(row => <tr key={rowKey(row)}>
              {columns.map(col => <td key={col} className={cellClassName(row[col])}>{processBadges(row[col])}</td>)}
            </tr>)}
        </tbody>
      </table>
    </div>;
};

Endor Labs hooks for Claude Code send a structured event for every session, user prompt, tool call, shell command, and file operation. The hook also enforces your Coding Agent Governance policies locally before each action reaches the agent.

<Note>
  See [Endor Labs MCP server in Claude Code](/setup-deployment/mcp/claude-code) to query Endor Labs from the assistant. See [Endor Labs Agent Kit in Claude Code](/secure-ai-coding/agent-kit/claude-code) to install workflow agents. See [Deploy hooks for Claude Code](/agent-governance/claude-code) to capture events and enforce Coding Agent Governance policies.
</Note>

## Before you begin

Confirm the following requirements before you install the hook:

* An admin has completed the [prerequisites](/agent-governance/prerequisites), including creating an API key with the **AI Audit User** role.
* The target machine runs macOS or Linux with Claude Code installed. For Windows, generate the configuration.
* [endorctl](/setup-deployment/cli) is on `PATH`. This applies to the hand-written configuration below. A generated configuration installs and updates endorctl automatically at session start.

<Warning>
  Do not export `ENDOR_TOKEN` in the environment that launches Claude Code. Hooks authenticate with the API key and secret only, and on older versions of endorctl the extra token triggered a mixed-authentication error that blocked `endorctl ai-audit`.
</Warning>

<Tip>
  Prefer generating this file over maintaining it by hand. The [in-browser generator](/agent-governance/mdm-deployment#generate-a-configuration-in-your-browser) assembles `settings.json` (or a macOS configuration profile) from the [mdm-scripts repository](https://github.com/endorlabs/mdm-scripts/tree/main/agent-governance) sources, and adds a bootstrap that installs and updates endorctl at session start.
</Tip>

Claude Code reads its credentials and namespace from the `env` block in `settings.json`, so the configuration is self-contained. Replace the placeholder values in `env` with your namespace and an API key issued for the **AI Audit User** role. The `env` block also sets `ENDOR_AI_AUDIT_CACHE_ENABLED` to `true`, so the hook batches events locally and flushes them at `SessionEnd`. Use the following configuration for macOS and Linux.

```json expandable theme={null}
{
  "env": {
    "ENDOR_API": "https://api.endorlabs.com",
    "ENDOR_NAMESPACE": "<your-namespace>",
    "ENDOR_API_CREDENTIALS_KEY": "<your-api-key>",
    "ENDOR_API_CREDENTIALS_SECRET": "<your-api-secret>",
    "ENDOR_AI_AUDIT_CACHE_ENABLED": "true"
  },
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "endorctl --api $ENDOR_API --namespace $ENDOR_NAMESPACE --api-key $ENDOR_API_CREDENTIALS_KEY --api-secret $ENDOR_API_CREDENTIALS_SECRET ai-audit claudecode"
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "endorctl --api $ENDOR_API --namespace $ENDOR_NAMESPACE --api-key $ENDOR_API_CREDENTIALS_KEY --api-secret $ENDOR_API_CREDENTIALS_SECRET ai-audit claudecode"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "endorctl --api $ENDOR_API --namespace $ENDOR_NAMESPACE --api-key $ENDOR_API_CREDENTIALS_KEY --api-secret $ENDOR_API_CREDENTIALS_SECRET ai-audit claudecode"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "endorctl --api $ENDOR_API --namespace $ENDOR_NAMESPACE --api-key $ENDOR_API_CREDENTIALS_KEY --api-secret $ENDOR_API_CREDENTIALS_SECRET ai-audit claudecode"
          }
        ]
      }
    ],
    "PostToolUseFailure": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "endorctl --api $ENDOR_API --namespace $ENDOR_NAMESPACE --api-key $ENDOR_API_CREDENTIALS_KEY --api-secret $ENDOR_API_CREDENTIALS_SECRET ai-audit claudecode"
          }
        ]
      }
    ],
    "ConfigChange": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "endorctl --api $ENDOR_API --namespace $ENDOR_NAMESPACE --api-key $ENDOR_API_CREDENTIALS_KEY --api-secret $ENDOR_API_CREDENTIALS_SECRET ai-audit claudecode"
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "endorctl --api $ENDOR_API --namespace $ENDOR_NAMESPACE --api-key $ENDOR_API_CREDENTIALS_KEY --api-secret $ENDOR_API_CREDENTIALS_SECRET ai-audit claudecode"
          }
        ]
      }
    ]
  }
}
```

On Windows, generate the file instead. See [Generate a configuration in your browser](/agent-governance/mdm-deployment#generate-a-configuration-in-your-browser) to produce it, or use the Claude Code tab under [Generate a configuration manually](/agent-governance/mdm-deployment#generate-a-configuration-manually).

## Install for one developer

Use this for a single-machine trial or a personal install.

1. Open `~/.claude/settings.json` in a text editor. Create the file if it does not exist.
2. Paste the configuration shown in [Before you begin](#before-you-begin) under the `hooks` key. Merge with any existing `hooks` entries, because Claude Code does not deduplicate handlers across configurations.
3. Save the file and restart any active Claude Code session.

## Install on a managed fleet

Claude Code reads enterprise hooks from a managed settings file: `/Library/Application Support/ClaudeCode/managed-settings.json` on macOS, or `/etc/claude-code/managed-settings.json` on Linux. Use your MDM, such as Jamf, Intune, Kandji, or JumpCloud, to deliver that file to each managed machine. The `env` block in the configuration above carries the Endor Labs credentials, so you don't need a separate shell setup.

<Note>
  On macOS, environment variables set in `~/.zshrc` reach apps launched from a terminal but not apps launched from Spotlight. Ship the variables through your MDM payload (for example, a `launchctl setenv` profile or an `~/.zprofile` snippet) for full coverage.
</Note>

<Tip>
  If your MDM's deployment script rejects the inline JSON payload, encode the configuration in Base64 in the script, and decode it on the target machine. This avoids the quoting and escaping issues that some MDMs introduce when publishing JSON.
</Tip>

## Per-repository overrides

Add `<repository>/.claude/settings.json` and commit it. Repository hooks layer on top of user and enterprise hooks and let security-sensitive projects extend the configuration. For developer-local additions outside source control, use `<repository>/.claude/settings.local.json`.

## Claude Code hook events used by Coding Agent Governance

The following table lists every event the hook processes.

<YamlTable>
  {`
    - Hook event: \`SessionStart\`
    Why it matters: Records the session and the agent version, model, and user for inventory. Refreshes the local policy cache.
    - Hook event: \`UserPromptSubmit\`
    Why it matters: Records that a prompt was submitted, for session counters in inventory. The prompt text stays on the developer's machine.
    - Hook event: \`PreToolUse\`
    Why it matters: Enforces policies before tools run. Bash commands run **Command Execution**. Read, Write, and Edit tools run **File Access**. \`mcp__*\` tools run **MCP Server Access**.
    - Hook event: \`PostToolUse\`
    Why it matters: Captures completed tool calls for inventory and post-action audit, including MCP tool responses.
    - Hook event: \`PostToolUseFailure\`
    Why it matters: Captures failed tool calls for inventory.
    - Hook event: \`ConfigChange\`
    Why it matters: Enforces **File Access** edit policies when a Claude Code configuration file changes, complementing the agent self-configuration system policy.
    - Hook event: \`SessionEnd\`
    Why it matters: Flushes cached events to Endor Labs when the session ends.
    `}
</YamlTable>

The hook accepts and ignores Claude Code events outside this set, such as `Stop`, `PermissionRequest`, and `SubagentStart`, if a configuration registers them. An older configuration keeps working, but those events record nothing.

When you [connect the Anthropic Compliance API](/agent-governance/claude-code/compliance-api), the `SessionStart` hook resolves the developer's organization identity from their Claude account email. Session policies can then match on the developer's organization roles and groups.

The `PreToolUse` hook fails open. If the hook itself errors out, Claude Code allows the tool call rather than blocking it and keeps working. The usual cause is a missing binary.

An unreadable local policy cache does not disable enforcement, because the hook falls back to the built-in system policies. A successfully evaluated **Block** policy still stops the tool call.

## Tune the hook

For an audit-only rollout, set `ENDOR_AI_AUDIT_NO_BLOCKING=true` in the {agent_0} hook environment. [endorctl ai-audit](/developers-api/cli/commands/ai-audit) then downgrades every `Block` action to `Alert` at evaluation time. For Cursor, set it in the shell that launches the IDE. For Claude Code, set it in the `env` block of `settings.json`. Policies still record violations under **Policy Violations**, but the hook never denies a {actionTerm_0}. Use audit-only mode to seed a new policy without interrupting developers, then unset the variable when you're ready to enforce.

Any value that Go parses as a `bool` (such as `true`, `1`, or `t`) turns the option on. Unset, empty, or unparseable values keep enforcement on.

## Verify hooks are firing

1. Start a session in {agent_1} and ask the agent to run a benign shell command, such as `ls`.
2. Select **Agent Governance** from the left sidebar, then select **Inventory**. The page opens on the **Workstation Inventory** tab.
3. Confirm the developer's {agent_1} row shows a recent value under **Last Active**.

If no row appears, see [Troubleshoot a quiet machine](#troubleshoot-a-quiet-machine).

## What developers see when a policy fires

When a policy with the **Block** action matches an event, the hook returns a deny response to {agent_2}. {agent_2} stops the {actionTerm_1} and surfaces the **User Message** to the developer when the hook response can carry it. A response that carries a message also explains that a governance policy denied the action and tells the agent not to work around the block.

When a policy with the **Ask Permission** action matches, {agent_2} asks the developer to confirm or deny, where it can pause at that event. Where it cannot, the outcome depends on the event. Endor Labs records the match under **Policy Violations** as **Ask** either way. See [Enforcement behavior](/agent-governance/how-it-works#enforcement-behavior) to review the per-agent behavior.

When a policy with the **Alert** action matches, the hook allows the action to proceed. Endor Labs records the event under **Policy Violations** for your security team to review.

## Troubleshoot a quiet machine

Run these checks if a developer's actions never appear under **Workstation Inventory** or **Policy Violations**:

* Confirm the developer has started at least one session. With the event cache enabled, the hook batches events and flushes them within about 30 seconds of hook activity. A full flush runs at session start and end. Inventory **Sessions**, **Last Active**, and counts can therefore lag the newest actions by that interval.
* Hook errors fail open. If the hook cannot run or reach the Endor Labs API, {agent_3} allows the {actionTerm_2}. The event is missing from inventory rather than surfacing an error. The remaining checks rule out the common causes.
* Confirm endorctl is on `PATH` in the environment that runs the hook. The hook fails silently if the binary is not found. Run `endorctl version` from the same shell to verify.
* Confirm only one endorctl installation is active. If `endorctl ai-audit` fails with `Failed with non-blocking status code: No stderr output`, conflicting installations are the likely cause, such as an `npx`-provisioned endorctl alongside a Homebrew install. Keep one installation method per machine, remove the others, then run `endorctl version` to confirm.
* Confirm `ENDOR_API`, `ENDOR_NAMESPACE`, `ENDOR_API_CREDENTIALS_KEY`, and `ENDOR_API_CREDENTIALS_SECRET` are set for the process that runs the hook. Cursor reads them from the shell that launched it. Claude Code reads them from the `env` block in `settings.json`.
* Confirm `ENDOR_TOKEN` is not also exported alongside your Coding Agent Governance API key credentials in the hook environment. Hooks rely only on `ENDOR_API_CREDENTIALS_KEY` and `ENDOR_API_CREDENTIALS_SECRET`. `ENDOR_TOKEN` is redundant and, on older versions of endorctl, triggered a mixed-authentication error that blocked `endorctl ai-audit`.
* Confirm the API key has the **AI Audit User** role and has not expired.
* Confirm the developer restarted {agent_3} after the hooks file changed. {agent_3} reads the configuration at session start.
* Confirm the developer restarted {agent_3} after a policy edit. The local policy cache refreshes at session start.
* Confirm the JSON file parses. A trailing comma silently disables every hook in the file.
* Confirm the Endor Labs API is reachable from the environment that runs the hook. Network failures appear as gaps in the inventory.

## Next steps

With hooks deployed, continue with the following pages:

* See [Triage policy violations](/agent-governance/policy-violations) to set your daily review workflow.
* See [Read the Coding Agent Governance overview](/agent-governance/overview) to track adoption and enforcement trends.
