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

> Install Endor Labs hooks for the Codex CLI on macOS, Linux, and Windows developer machines to capture session, command, and file events.

export const agent_3 = "Codex"

export const actionTerm_2 = "action"

export const agent_2 = "Codex"

export const actionTerm_1 = "action"

export const agent_1 = "Codex"

export const agent_0 = "Codex"

export const actionTerm_0 = "action"

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 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>;
    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);
  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]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <table>
      <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>;
};

Endor Labs hooks for Codex send a structured event for every session, prompt, shell command, file patch, and MCP tool call the Codex CLI makes. The hook also enforces your Coding Agent Governance policies locally before each action runs.

## 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, Linux, or Windows with the Codex CLI installed.
* endorctl is on `PATH`. See [endorctl CLI](/setup-deployment/cli).
* The shell that launches Codex has these environment variables set:
  * `ENDOR_API` (for example, `https://api.endorlabs.com`)
  * `ENDOR_NAMESPACE`
  * `ENDOR_API_CREDENTIALS_KEY`
  * `ENDOR_API_CREDENTIALS_SECRET`

<Warning>
  Do not export `ENDOR_TOKEN` in the environment that runs the hook. 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 configuration over maintaining it by hand. The [in-browser generator](/agent-governance/mdm-deployment#generate-a-configuration-in-your-browser) assembles a Codex `requirements.toml` (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>

Codex reads hooks from the `[hooks]` table in `config.toml`. Use the following configuration.

```toml expandable theme={null}
[[hooks.SessionStart]]
[[hooks.SessionStart.hooks]]
type = "command"
command = "ENDOR_AI_AUDIT_CACHE_ENABLED=true endorctl --api $ENDOR_API --namespace $ENDOR_NAMESPACE --api-key $ENDOR_API_CREDENTIALS_KEY --api-secret $ENDOR_API_CREDENTIALS_SECRET ai-audit codex"

[[hooks.UserPromptSubmit]]
[[hooks.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 codex"

[[hooks.PreToolUse]]
matcher = ".*"
[[hooks.PreToolUse.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 codex"

[[hooks.PermissionRequest]]
[[hooks.PermissionRequest.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 codex"

[[hooks.PostToolUse]]
matcher = ".*"
[[hooks.PostToolUse.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 codex"

[[hooks.Stop]]
[[hooks.Stop.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 codex"
```

<Note>
  Codex also accepts the same events in a sibling `hooks.json` file, and it runs both sources additively. Define each event in one place only, or the hook fires twice per event.
</Note>

## Install for one developer

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

1. Open `~/.codex/config.toml` in a text editor. Create the file if it does not exist.
2. Append the `[hooks]` configuration shown in [Before you begin](#before-you-begin).
3. Save the file and start a new Codex session.

If `CODEX_HOME` is set, edit `config.toml` in that directory instead.

## Install on a managed fleet

Codex reads system-managed configuration from a fixed directory per platform. Use your MDM, such as Jamf, Intune, Kandji, or JumpCloud, to deliver the hook configuration to each managed machine. Also deliver the four Endor Labs environment variables in the shell that launches Codex.

* On macOS and Linux, deliver `/etc/codex/hooks.json`, or add the `[hooks]` table to `/etc/codex/requirements.toml`. Settings in `requirements.toml` take precedence over the other configuration layers.
* On Windows, deliver `%ProgramData%\OpenAI\Codex\hooks.json` or `%ProgramData%\OpenAI\Codex\requirements.toml`.

<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 the `[hooks]` table to `<repository>/.codex/config.toml` and commit it. Repository hooks let security-sensitive projects extend the configuration.

Codex loads repository configuration only after the developer trusts the project. A committed hook configuration takes effect once the project's `trust_level` is `trusted` in the developer's Codex configuration.

## Codex hook events used by Coding Agent Governance

<YamlTable>
  {`
    - Hook event: \`SessionStart\`
    Why it matters: Records the session, agent, model, and user for inventory. Captures the configuration, MCP server, and skills snapshot, and refreshes the local policy cache. Unlike Claude Code and Cursor, Codex fires this hook when the first prompt is submitted, not when the session opens, so this snapshot lands with the first prompt. Also fires on resume and compaction.
    - 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 a tool runs. Shell commands map to **Command Execution**, \`apply_patch\` file changes map to **File Access**, and MCP tools map to **MCP Server Access**.
    - Hook event: \`PermissionRequest\`
    Why it matters: Enforces policies at Codex's own approval prompts, such as shell escalation and network access requests.
    - Hook event: \`PostToolUse\`
    Why it matters: Captures completed shell, file, and MCP tool calls for inventory, including MCP responses.
    - Hook event: \`Stop\`
    Why it matters: Flushes cached events to Endor Labs. Fires once per turn, because Codex has no session-end event.
    `}
</YamlTable>

Enforcement hooks fail open. If the hook itself errors out, Codex allows the action 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 action.

Codex handles the **Ask Permission** action differently from other agents:

* At `PreToolUse`, Codex honors only a deny response and cannot pause for approval. An **Ask Permission** policy fails closed and denies the call, carrying the ask message so the developer sees why.
* At `PermissionRequest`, an **Ask Permission** policy defers to Codex's own approval prompt, which asks the developer directly. Only a **Block** policy denies the escalation outright.

Codex reports file changes through its `apply_patch` tool. **File Access** policies match each file in the patch individually, and a `Write`, `Edit`, or `apply_patch` matcher all target the same tool.

## 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 this to seed a new policy without interrupting developers, then unset the variable when you're ready to enforce.

Any value parsable as a Go 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 **Workstation Inventory**.
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. The agent receives a deny response that carries the **User Message**, 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, the hook returns an ask response. {agent_2} pauses and asks the developer to confirm or deny before the agent proceeds.

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, and fully at session start and end, so inventory **Sessions**, **Last Active**, and counts can 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.
