> ## 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 Coding Agent Governance with MDM

> Generate Cursor hooks, Claude Code settings, Codex requirements, or a macOS configuration profile for managed developer machines.

export const AgentGovernanceConfigGenerator = ({owner = 'endorlabs', repo = 'mdm-scripts', branch = 'main', basePath = 'agent-governance'}) => {
  const CACHE_TTL = 60 * 60 * 1000;
  const rawUrl = rel => 'https://raw.githubusercontent.com/' + owner + '/' + repo + '/' + branch + '/' + basePath + '/' + rel;
  const FILES = {
    bootSh: 'scripts/download_endorctl.sh',
    bootPs1: 'scripts/download_endorctl.ps1'
  };
  const PINS = {
    render: '117b3dfbedc088098f69fcc6288516c4b4fe9446',
    plist: '20adbce17732627f2937a94d0d97443e8ecef148'
  };
  function shQuote(s) {
    return "'" + String(s).replace(/'/g, "'\\''") + "'";
  }
  function psQuote(s) {
    return "'" + String(s).replace(/'/g, "''") + "'";
  }
  function toUtf16leBase64(s) {
    const str = String(s);
    const bytes = [];
    for (let i = 0; i < str.length; i++) {
      const c = str.charCodeAt(i);
      bytes.push(c & 0xff, c >> 8 & 0xff);
    }
    const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    let out = '';
    for (let i = 0; i < bytes.length; i += 3) {
      const a = bytes[i];
      const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
      const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
      const n = a << 16 | b << 8 | c;
      out += alphabet[n >> 18 & 63];
      out += alphabet[n >> 12 & 63];
      out += i + 1 < bytes.length ? alphabet[n >> 6 & 63] : '=';
      out += i + 2 < bytes.length ? alphabet[n & 63] : '=';
    }
    return out;
  }
  function jqJson(value) {
    return JSON.stringify(value, null, 2);
  }
  function stripTrailingNewlines(s) {
    return String(s).replace(/\n+$/, '');
  }
  function jsEscape(s) {
    const esc = String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
    const lines = esc.split('\n');
    if (lines.length > 1 && lines[lines.length - 1] === '') lines.pop();
    return lines.join('\\n');
  }
  const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
  function buildEnvLines(envs) {
    const lines = ['ENDOR_AI_AUDIT_CACHE_ENABLED=true'];
    const list = Array.isArray(envs) ? envs : [];
    for (let i = 0; i < list.length; i++) {
      const key = list[i].key;
      const value = list[i].value;
      if (typeof key !== 'string' || !ENV_KEY_RE.test(key)) {
        throw new Error('--env key must match [A-Za-z_][A-Za-z0-9_]* (got: ' + key + ')');
      }
      const next = [];
      for (let j = 0; j < lines.length; j++) {
        if (!lines[j].startsWith(key + '=')) next.push(lines[j]);
      }
      next.push(key + '=' + String(value));
      lines.length = 0;
      for (let j = 0; j < next.length; j++) lines.push(next[j]);
    }
    return lines;
  }
  function envLinesToObject(lines) {
    const obj = Object.create(null);
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      if (!line) continue;
      const eq = line.indexOf('=');
      if (eq < 0) continue;
      obj[line.slice(0, eq)] = line.slice(eq + 1);
    }
    return obj;
  }
  function envPrefixFromObject(envObj) {
    const keys = Object.keys(envObj);
    let out = '';
    for (let i = 0; i < keys.length; i++) {
      const k = keys[i];
      out += k + '=' + shQuote(envObj[k]) + ' ';
    }
    return out;
  }
  function psEnvSetsFromLines(lines) {
    const parts = [];
    for (let i = 0; i < lines.length; i++) {
      const kv = lines[i];
      if (!kv) continue;
      const eq = kv.indexOf('=');
      const key = kv.slice(0, eq);
      const value = kv.slice(eq + 1);
      parts.push('$env:' + key + ' = ' + psQuote(value));
    }
    return parts.join('\n');
  }
  function psenc(src) {
    return 'powershell -NoProfile -EncodedCommand ' + toUtf16leBase64(src);
  }
  function render(state, sources) {
    const agent = state.agent;
    const targetOS = state.targetOS == null ? 'macos' : state.targetOS;
    const creds = state.creds || ({});
    const apiUrl = creds.apiUrl == null ? 'https://api.endorlabs.com' : creds.apiUrl;
    const apiKey = creds.apiKey;
    const apiSecret = creds.apiSecret;
    const namespace = creds.namespace;
    const skipUpdate = !!state.skipEndorctlUpdate;
    if (agent !== 'claude' && agent !== 'cursor' && agent !== 'codex') {
      throw new Error('unknown agent: ' + agent + ' (claude|cursor|codex)');
    }
    if (targetOS !== 'macos' && targetOS !== 'linux' && targetOS !== 'windows') {
      throw new Error('unknown --target-os: ' + targetOS + ' (macos|linux|windows)');
    }
    if (apiKey == null || apiKey === '') {
      throw new Error('api-key not provided');
    }
    if (apiSecret == null || apiSecret === '') {
      throw new Error('api-secret not provided');
    }
    if (namespace == null || namespace === '') {
      throw new Error('namespace not provided');
    }
    const envLines = buildEnvLines(state.envs);
    const envObj = envLinesToObject(envLines);
    const envPrefix = envPrefixFromObject(envObj);
    const psEnvSets = psEnvSetsFromLines(envLines);
    let boot = targetOS === 'windows' ? stripTrailingNewlines(sources.bootPs1) : stripTrailingNewlines(sources.bootSh);
    if (skipUpdate) {
      if (targetOS === 'windows') {
        boot = "$env:ENDORCTL_SKIP_UPDATE = '1'\n" + boot;
      } else {
        boot = 'ENDORCTL_SKIP_UPDATE=1\n' + boot;
      }
    }
    const subcmd = agent === 'claude' ? 'claudecode' : agent === 'cursor' ? 'cursor' : 'codex';
    const posixAudit = '"$HOME/.endorctl/endorctl" --api "$AGENT_HOOK_ENDOR_API" --namespace "$AGENT_HOOK_ENDOR_NAMESPACE" --api-key "$AGENT_HOOK_ENDOR_API_CREDENTIALS_KEY" --api-secret "$AGENT_HOOK_ENDOR_API_CREDENTIALS_SECRET" ai-audit ' + subcmd;
    const psBin = '& "$env:USERPROFILE\\.endorctl\\endorctl.exe"';
    const psAudit = psBin + ' --api "$env:AGENT_HOOK_ENDOR_API" --namespace "$env:AGENT_HOOK_ENDOR_NAMESPACE" --api-key "$env:AGENT_HOOK_ENDOR_API_CREDENTIALS_KEY" --api-secret "$env:AGENT_HOOK_ENDOR_API_CREDENTIALS_SECRET" ai-audit ' + subcmd + '; exit $LASTEXITCODE';
    const psAuditEvent = '$ProgressPreference = "SilentlyContinue"\n' + '$OutputEncoding = [System.Text.UTF8Encoding]::new($false)\n' + '$in = if ([Console]::IsInputRedirected) { [System.IO.StreamReader]::new([Console]::OpenStandardInput(), $OutputEncoding).ReadToEnd() } else { "" }\n' + '$in | ' + psAudit;
    let cmdAudit;
    let cmdSession;
    const pair = agent + ':' + targetOS;
    if (pair === 'claude:macos' || pair === 'claude:linux') {
      cmdAudit = posixAudit;
      cmdSession = boot + '\n' + posixAudit;
    } else if (pair === 'cursor:macos' || pair === 'cursor:linux') {
      cmdAudit = posixAudit;
      cmdSession = 'umask 077\n' + 'T="$HOME/.endorctl-cursor-stdin.$$"\n' + 'cat > "$T"\n' + 'chmod 600 "$T"\n' + "trap 'rm -f \"$T\"' EXIT\n" + boot + '\n' + envPrefix + '"$HOME/.endorctl/endorctl" --api ' + shQuote(apiUrl) + ' --namespace ' + shQuote(namespace) + ' --api-key ' + shQuote(apiKey) + ' --api-secret ' + shQuote(apiSecret) + ' ai-audit cursor < "$T"';
    } else if (pair === 'claude:windows') {
      cmdAudit = psenc(psAuditEvent);
      cmdSession = psenc(boot + '\n' + psAudit);
    } else if (pair === 'cursor:windows') {
      cmdAudit = psenc(psAuditEvent);
      cmdSession = psenc('$OutputEncoding = [System.Text.UTF8Encoding]::new($false)\n' + '$in = [System.IO.StreamReader]::new([Console]::OpenStandardInput(), $OutputEncoding).ReadToEnd()\n' + boot + '\n' + psEnvSets + '\n' + '$in | ' + psBin + ' --api ' + psQuote(apiUrl) + ' --namespace ' + psQuote(namespace) + ' --api-key ' + psQuote(apiKey) + ' --api-secret ' + psQuote(apiSecret) + ' ai-audit cursor; exit $LASTEXITCODE');
    } else if (pair === 'codex:macos' || pair === 'codex:linux') {
      cmdAudit = envPrefix + '"$HOME/.endorctl/endorctl" --api ' + shQuote(apiUrl) + ' --namespace ' + shQuote(namespace) + ' --api-key ' + shQuote(apiKey) + ' --api-secret ' + shQuote(apiSecret) + ' ai-audit codex';
      cmdSession = boot + '\n' + cmdAudit;
    } else if (pair === 'codex:windows') {
      cmdAudit = psenc('$ProgressPreference = "SilentlyContinue"\n' + '$OutputEncoding = [System.Text.UTF8Encoding]::new($false)\n' + '$in = if ([Console]::IsInputRedirected) { [System.IO.StreamReader]::new([Console]::OpenStandardInput(), $OutputEncoding).ReadToEnd() } else { "" }\n' + psEnvSets + '$in | ' + psBin + ' --api ' + psQuote(apiUrl) + ' --namespace ' + psQuote(namespace) + ' --api-key ' + psQuote(apiKey) + ' --api-secret ' + psQuote(apiSecret) + ' ai-audit codex; exit $LASTEXITCODE');
      cmdSession = psenc('$OutputEncoding = [System.Text.UTF8Encoding]::new($false)\n' + '$in = [System.IO.StreamReader]::new([Console]::OpenStandardInput(), $OutputEncoding).ReadToEnd()\n' + boot + '\n' + psEnvSets + '$in | ' + psBin + ' --api ' + psQuote(apiUrl) + ' --namespace ' + psQuote(namespace) + ' --api-key ' + psQuote(apiKey) + ' --api-secret ' + psQuote(apiSecret) + ' ai-audit codex; exit $LASTEXITCODE');
    }
    if (agent === 'codex') {
      const codexEvent = (event, cmd, matcher) => '[[hooks.' + event + ']]\n' + (matcher ? 'matcher = "' + matcher + '"\n' : '') + '[[hooks.' + event + '.hooks]]\ntype = "command"\ncommand = "' + cmd + '"\n\n';
      const tomlSession = jsEscape(cmdSession);
      const tomlAudit = jsEscape(cmdAudit);
      return '# Endor Labs AI governance - managed Codex hooks (generated by render.sh).\n' + '# Deliver as an MDM profile (com.openai.codex requirements_toml_base64) via\n' + '# "render-plist.sh --style mcx", or as a managed /etc/codex/requirements.toml.\n' + '# Hooks from a managed/requirements source are auto-trusted and cannot be\n' + '# disabled by the user; audit credentials are baked into each command. These\n' + '# run alongside any user/project hooks (like Claude/Cursor); to instead run\n' + '# only managed hooks, an admin can add "allow_managed_hooks_only = true".\n' + '[features]\nhooks = true\n\n' + codexEvent('SessionStart', tomlSession, '') + codexEvent('UserPromptSubmit', tomlAudit, '') + codexEvent('PreToolUse', tomlAudit, '.*') + codexEvent('PermissionRequest', tomlAudit, '.*') + codexEvent('PostToolUse', tomlAudit, '.*') + codexEvent('Stop', tomlAudit, '');
    }
    let doc;
    if (agent === 'cursor') {
      const hook = c => ({
        command: c
      });
      doc = {
        version: 1,
        hooks: {
          sessionStart: [hook(cmdSession)],
          sessionEnd: [hook(cmdAudit)],
          beforeSubmitPrompt: [hook(cmdAudit)],
          preToolUse: [hook(cmdAudit)],
          postToolUse: [hook(cmdAudit)],
          postToolUseFailure: [hook(cmdAudit)],
          beforeShellExecution: [hook(cmdAudit)],
          afterShellExecution: [hook(cmdAudit)],
          beforeMCPExecution: [hook(cmdAudit)],
          beforeReadFile: [hook(cmdAudit)],
          afterFileEdit: [hook(cmdAudit)],
          stop: [hook(cmdAudit)]
        }
      };
    } else {
      const hook = c => ({
        hooks: [{
          type: 'command',
          command: c
        }]
      });
      const mhook = c => {
        const h = hook(c);
        h.matcher = '.*';
        return h;
      };
      doc = {
        env: {
          AGENT_HOOK_ENDOR_API: apiUrl,
          AGENT_HOOK_ENDOR_API_CREDENTIALS_KEY: apiKey,
          AGENT_HOOK_ENDOR_API_CREDENTIALS_SECRET: apiSecret,
          AGENT_HOOK_ENDOR_NAMESPACE: namespace,
          ...envObj
        },
        hooks: {
          SessionStart: [hook(cmdSession)],
          UserPromptSubmit: [hook(cmdAudit)],
          PreToolUse: [mhook(cmdAudit)],
          PostToolUse: [mhook(cmdAudit)],
          PostToolUseFailure: [mhook(cmdAudit)],
          Stop: [hook(cmdAudit)]
        }
      };
    }
    return jqJson(doc) + '\n';
  }
  function xmlEscape(s) {
    return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }
  function emitPlistValue(value, depth, lines) {
    const pad = ('\t').repeat(depth);
    if (value === true) {
      lines.push(pad + '<true/>');
      return;
    }
    if (value === false) {
      lines.push(pad + '<false/>');
      return;
    }
    if (typeof value === 'string') {
      lines.push(pad + '<string>' + xmlEscape(value) + '</string>');
      return;
    }
    if (typeof value === 'number') {
      if (!Number.isFinite(value)) {
        throw new Error('unsupported plist number: ' + value);
      }
      if (Number.isInteger(value)) {
        lines.push(pad + '<integer>' + value + '</integer>');
      } else {
        lines.push(pad + '<real>' + value + '</real>');
      }
      return;
    }
    if (Array.isArray(value)) {
      if (value.length === 0) {
        lines.push(pad + '<array/>');
        return;
      }
      lines.push(pad + '<array>');
      for (let i = 0; i < value.length; i++) {
        emitPlistValue(value[i], depth + 1, lines);
      }
      lines.push(pad + '</array>');
      return;
    }
    if (value !== null && typeof value === 'object') {
      const keys = Object.keys(value).sort();
      if (keys.length === 0) {
        lines.push(pad + '<dict/>');
        return;
      }
      lines.push(pad + '<dict>');
      const childPad = ('\t').repeat(depth + 1);
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i];
        lines.push(childPad + '<key>' + xmlEscape(key) + '</key>');
        emitPlistValue(value[key], depth + 1, lines);
      }
      lines.push(pad + '</dict>');
      return;
    }
    throw new Error('unsupported plist value type: ' + typeof value);
  }
  function toPlistXml1(root) {
    const lines = ['<?xml version="1.0" encoding="UTF-8"?>', '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">', '<plist version="1.0">'];
    emitPlistValue(root, 0, lines);
    lines.push('</plist>');
    return lines.join('\n') + '\n';
  }
  function toUtf8Base64(s) {
    const str = String(s);
    const bytes = [];
    for (let i = 0; i < str.length; i++) {
      let code = str.codePointAt(i);
      if (code > 0xffff) i++;
      if (code < 0x80) {
        bytes.push(code);
      } else if (code < 0x800) {
        bytes.push(0xc0 | code >> 6, 0x80 | code & 63);
      } else if (code < 0x10000) {
        bytes.push(0xe0 | code >> 12, 0x80 | code >> 6 & 63, 0x80 | code & 63);
      } else {
        bytes.push(0xf0 | code >> 18, 0x80 | code >> 12 & 63, 0x80 | code >> 6 & 63, 0x80 | code & 63);
      }
    }
    const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    let out = '';
    for (let i = 0; i < bytes.length; i += 3) {
      const a = bytes[i];
      const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
      const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
      const n = a << 16 | b << 8 | c;
      out += alphabet[n >> 18 & 63];
      out += alphabet[n >> 12 & 63];
      out += i + 1 < bytes.length ? alphabet[n >> 6 & 63] : '=';
      out += i + 2 < bytes.length ? alphabet[n & 63] : '=';
    }
    return out;
  }
  function renderPlist(config, opts) {
    if (!opts || typeof opts !== 'object') {
      throw new Error('opts is required');
    }
    const style = opts.style == null ? 'plist' : opts.style;
    if (style !== 'plist' && style !== 'mcx') {
      throw new Error('unknown --style: ' + style + ' (plist|mcx)');
    }
    const identifier = opts.identifier;
    const organization = opts.organization;
    const profileUuid = opts.profileUuid;
    const contentUuid = opts.contentUuid;
    if (!identifier) throw new Error('--identifier is required');
    if (!organization) throw new Error('--organization is required');
    if (!profileUuid) throw new Error('profileUuid is required');
    if (!contentUuid) throw new Error('contentUuid is required');
    const name = opts.name == null ? 'Endor AI Governance' : opts.name;
    const payloadType = opts.payloadType == null ? 'com.anthropic.claudecode' : opts.payloadType;
    const profileIdentifier = opts.profileIdentifier == null || opts.profileIdentifier === '' ? identifier : opts.profileIdentifier;
    if (style === 'mcx') {
      const prefDomain = opts.prefDomain == null ? 'com.openai.codex' : opts.prefDomain;
      const prefKey = opts.prefKey == null ? 'requirements_toml_base64' : opts.prefKey;
      const raw = String(config).replace(/\n+$/, '');
      if (!raw) {
        throw new Error('stdin is empty (pipe: render.sh --agent codex ... -o -)');
      }
      const forced = {};
      forced[prefKey] = toUtf8Base64(raw);
      const prefContent = {};
      prefContent[prefDomain] = {
        Forced: [{
          mcx_preference_settings: forced
        }]
      };
      const mcxInner = {
        PayloadDescription: 'Managed preferences (Endor Labs AI governance hooks) for the agent.',
        PayloadDisplayName: name,
        PayloadEnabled: true,
        PayloadIdentifier: identifier + '.settings',
        PayloadType: 'com.apple.ManagedClient.preferences',
        PayloadUUID: contentUuid,
        PayloadVersion: 1,
        PayloadContent: prefContent
      };
      const mcxProfile = {
        PayloadContent: [mcxInner],
        PayloadDescription: 'Deploys managed AI-tool preferences for Endor Labs AI auditing.',
        PayloadDisplayName: name,
        PayloadIdentifier: profileIdentifier,
        PayloadOrganization: organization,
        PayloadScope: 'System',
        PayloadType: 'Configuration',
        PayloadUUID: profileUuid,
        PayloadVersion: 1
      };
      return toPlistXml1(mcxProfile);
    }
    const settings = typeof config === 'string' ? JSON.parse(config) : config;
    if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
      throw new Error('stdin is not a JSON settings object');
    }
    const inner = {
      PayloadDescription: 'Managed settings (env + hooks) for Endor Labs AI governance auditing.',
      PayloadDisplayName: name,
      PayloadEnabled: true,
      PayloadIdentifier: identifier + '.settings',
      PayloadType: payloadType,
      PayloadUUID: contentUuid,
      PayloadVersion: 1,
      ...settings
    };
    const profile = {
      PayloadContent: [inner],
      PayloadDescription: 'Deploys managed AI-tool settings for Endor Labs AI auditing.',
      PayloadDisplayName: name,
      PayloadIdentifier: profileIdentifier,
      PayloadOrganization: organization,
      PayloadScope: 'System',
      PayloadType: 'Configuration',
      PayloadUUID: profileUuid,
      PayloadVersion: 1
    };
    return toPlistXml1(profile);
  }
  const DEFAULT_API_URL = 'https://api.endorlabs.com';
  const CACHE_ENV_KEY = 'ENDOR_AI_AUDIT_CACHE_ENABLED';
  const MONITOR_ENV_KEY = 'ENDOR_AI_AUDIT_NO_BLOCKING';
  const PAYLOAD_TYPE_DEFAULT = 'com.anthropic.claudecode';
  const MCX_DOMAIN_DEFAULT = 'com.openai.codex';
  const PROFILE_NAME_DEFAULT = 'Endor AI Governance';
  const UPSTREAM_URL = 'https://github.com/endorlabs/mdm-scripts/tree/main/agent-governance';
  const [isDark, setIsDark] = useState(false);
  const [sources, setSources] = useState(null);
  const [status, setStatus] = useState('loading');
  const [agent, setAgent] = useState('claude');
  const [targetOS, setTargetOS] = useState('macos');
  const [apiKey, setApiKey] = useState('');
  const [apiSecret, setApiSecret] = useState('');
  const [namespace, setNamespace] = useState('');
  const [apiUrl, setApiUrl] = useState(DEFAULT_API_URL);
  const [envs, setEnvs] = useState([{
    key: CACHE_ENV_KEY,
    value: 'true'
  }]);
  const [skipEndorctlUpdate, setSkipEndorctlUpdate] = useState(false);
  const [wantProfile, setWantProfile] = useState(false);
  const [profileIdentifier, setProfileIdentifier] = useState('com.example.ai-governance.claudecode');
  const [profileOrganization, setProfileOrganization] = useState('');
  const [profileName, setProfileName] = useState(PROFILE_NAME_DEFAULT);
  const [profilePayloadType, setProfilePayloadType] = useState(PAYLOAD_TYPE_DEFAULT);
  const [profileIdentifierOverride, setProfileIdentifierOverride] = useState('');
  const [copied, setCopied] = useState(false);
  const [expanded, setExpanded] = useState(false);
  useEffect(() => {
    const check = () => {
      const r = document.documentElement;
      setIsDark(r.dataset.theme === 'dark' || r.classList.contains('dark'));
    };
    check();
    const obs = new MutationObserver(check);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme', 'class']
    });
    return () => obs.disconnect();
  }, []);
  useEffect(() => {
    let cancelled = false;
    const cacheKey = 'ag-gen-' + owner + '/' + repo + '@' + branch;
    try {
      const raw = sessionStorage.getItem(cacheKey);
      if (raw) {
        const c = JSON.parse(raw);
        if (Date.now() - c.ts < CACHE_TTL) {
          setSources(c.data);
          setStatus('ready');
          return undefined;
        }
      }
    } catch (e) {
      console.debug('ag-gen: cache read failed', e);
    }
    setStatus('loading');
    Promise.all(Object.entries(FILES).map(([k, rel]) => fetch(rawUrl(rel)).then(r => {
      if (!r.ok) throw new Error(rel + ': HTTP ' + r.status);
      return r.text();
    }).then(text => [k, text]))).then(pairs => {
      if (cancelled) return;
      const map = Object.fromEntries(pairs);
      try {
        sessionStorage.setItem(cacheKey, JSON.stringify({
          ts: Date.now(),
          data: map
        }));
      } catch (e) {
        console.debug('ag-gen: cache write failed', e);
      }
      setSources(map);
      setStatus('ready');
    }).catch(e => {
      if (!cancelled) {
        console.debug('ag-gen: source fetch failed', e, {
          pins: PINS
        });
        setStatus('error');
      }
    });
    return () => {
      cancelled = true;
    };
  }, [owner, repo, branch, basePath]);
  const profileAvailable = (agent === 'claude' || agent === 'codex') && targetOS === 'macos';
  const useProfile = profileAvailable && wantProfile;
  const profileStyle = agent === 'codex' ? 'mcx' : 'plist';
  const buildState = () => {
    const state = {
      agent,
      targetOS,
      creds: {
        apiKey: apiKey.trim(),
        apiSecret: apiSecret.trim(),
        namespace: namespace.trim(),
        apiUrl: apiUrl.trim() || DEFAULT_API_URL
      },
      envs: envs.filter(row => row.key.trim().length > 0).map(row => ({
        key: row.key.trim(),
        value: row.value
      })),
      skipEndorctlUpdate: !!skipEndorctlUpdate,
      profile: null
    };
    if (useProfile) {
      const id = profileIdentifier.trim();
      state.profile = {
        style: profileStyle,
        identifier: id,
        organization: profileOrganization.trim(),
        name: profileName.trim() || PROFILE_NAME_DEFAULT,
        payloadType: profilePayloadType.trim() || PAYLOAD_TYPE_DEFAULT,
        profileIdentifier: profileIdentifierOverride.trim() || id
      };
    }
    return state;
  };
  const state = buildState();
  const credsOk = !!(state.creds.apiKey && state.creds.apiSecret && state.creds.namespace);
  const profileOk = !useProfile || !!state.profile?.identifier && !!state.profile?.organization;
  const invalidEnvKeys = envs.map(row => row.key.trim()).filter(key => key.length > 0 && !ENV_KEY_RE.test(key));
  const validInputs = credsOk && profileOk && invalidEnvKeys.length === 0;
  const monitorOnlyOn = envs.some(row => row.key.trim() === MONITOR_ENV_KEY && row.value.trim() === 'true');
  const setMonitorOnly = on => {
    setEnvs(prev => {
      const without = prev.filter(row => row.key.trim() !== MONITOR_ENV_KEY);
      if (!on) return without.length ? without : [{
        key: CACHE_ENV_KEY,
        value: 'true'
      }];
      return [...without, {
        key: MONITOR_ENV_KEY,
        value: 'true'
      }];
    });
  };
  const updateEnvRow = (index, field, value) => {
    setEnvs(prev => prev.map((row, i) => i === index ? {
      ...row,
      [field]: value
    } : row));
  };
  const addEnvRow = () => {
    setEnvs(prev => [...prev, {
      key: '',
      value: ''
    }]);
  };
  const isSeededRow = row => row.key.trim() === CACHE_ENV_KEY;
  const removeEnvRow = index => {
    setEnvs(prev => {
      const next = prev.filter((_, i) => i !== index);
      return next.length ? next : [{
        key: CACHE_ENV_KEY,
        value: 'true'
      }];
    });
  };
  const outputMeta = () => {
    if (useProfile) {
      const base = agent === 'codex' ? MCX_DOMAIN_DEFAULT : (state.profile?.payloadType || PAYLOAD_TYPE_DEFAULT).replace(/[^A-Za-z0-9._-]/g, '_');
      return {
        fileName: base + '.mobileconfig',
        mime: 'application/x-apple-aspen-config',
        pathHint: agent === 'codex' ? 'Upload the .mobileconfig to your MDM as a Custom Profile (Codex managed requirements on macOS).' : 'Upload the .mobileconfig to your MDM as a Custom Profile (Claude managed settings on macOS).'
      };
    }
    const HINTS = {
      claude: {
        macos: 'User path: ~/.claude/settings.json. Fleet Linux path: /etc/claude-code/managed-settings.json',
        linux: 'Linux managed path: /etc/claude-code/managed-settings.json (also valid as ~/.claude/settings.json for a local trial).',
        windows: 'Push via Intune (or your MDM). Local trial path: ~/.claude/settings.json'
      },
      cursor: {
        macos: 'User path: ~/.cursor/hooks.json. Fleet macOS path: /Library/Application Support/Cursor/hooks.json',
        linux: 'Linux managed path: /etc/cursor/hooks.json (also valid as ~/.cursor/hooks.json for a local trial).',
        windows: 'Push via Intune (or your MDM). Local trial path: ~/.cursor/hooks.json'
      },
      codex: {
        macos: 'Managed path: /etc/codex/requirements.toml. For fleets, prefer the MDM profile output below.',
        linux: 'Managed path: /etc/codex/requirements.toml',
        windows: 'Push via Intune (or your MDM). Managed path: %ProgramData%\\OpenAI\\Codex\\requirements.toml'
      }
    };
    const FILE_NAMES = {
      claude: 'claude-settings.json',
      cursor: 'cursor-hooks.json',
      codex: 'codex-requirements.toml'
    };
    return {
      fileName: FILE_NAMES[agent],
      mime: agent === 'codex' ? 'application/toml' : 'application/json',
      pathHint: HINTS[agent][targetOS]
    };
  };
  let result = null;
  let assembleError = null;
  if (status === 'ready' && sources && validInputs) {
    try {
      const settingsJson = render(state, sources);
      if (useProfile && state.profile) {
        if (typeof crypto === 'undefined' || !crypto.randomUUID) {
          throw new Error('crypto.randomUUID unavailable');
        }
        result = renderPlist(settingsJson, {
          style: state.profile.style,
          identifier: state.profile.identifier,
          organization: state.profile.organization,
          name: state.profile.name,
          payloadType: state.profile.payloadType,
          profileIdentifier: state.profile.profileIdentifier,
          profileUuid: crypto.randomUUID().toUpperCase(),
          contentUuid: crypto.randomUUID().toUpperCase()
        });
      } else {
        result = settingsJson;
      }
    } catch (e) {
      console.debug('ag-gen: assemble failed', e);
      assembleError = true;
    }
  }
  const meta = outputMeta();
  const handleCopy = () => {
    if (!result) return;
    navigator.clipboard.writeText(result).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    }).catch(e => console.debug('ag-gen: copy failed', e));
  };
  const handleDownload = () => {
    if (!result) return;
    const blob = new Blob([result], {
      type: meta.mime
    });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = meta.fileName;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
  };
  const manualCmd = ['git clone https://github.com/endorlabs/mdm-scripts', 'cd mdm-scripts/agent-governance', 'scripts/render.sh --agent ' + agent + ' --target-os ' + targetOS + ' \\', '  --api-key "$ENDOR_API_CREDENTIALS_KEY" \\', '  --api-secret "$ENDOR_API_CREDENTIALS_SECRET" \\', '  --namespace "$ENDOR_NAMESPACE"' + (useProfile ? ' -o - \\\n| scripts/render-plist.sh ' + (profileStyle === 'mcx' ? '--style mcx ' : '') + '\\\n  --identifier <reverse.dns.id> --organization "<Org>" \\\n  -o ' + (profileStyle === 'mcx' ? MCX_DOMAIN_DEFAULT : PAYLOAD_TYPE_DEFAULT) + '.mobileconfig' : ' -o ' + meta.fileName)].join('\n');
  const bg = isDark ? '#0d1117' : '#ffffff';
  const bgLight = isDark ? '#161b22' : '#f6f8fa';
  const text = isDark ? '#e6edf3' : '#1f2937';
  const muted = isDark ? 'rgba(230,237,243,0.7)' : 'rgba(31,41,55,0.7)';
  const GRAD = 'linear-gradient(to bottom, rgba(50,225,140,0.92), rgba(30,190,110,0.92))';
  const activeFg = isDark ? '#000' : '#fff';
  const secondaryBg = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.4)';
  const BORDER = '1px solid rgba(38,208,124,0.45)';
  const GRID = 'repeat(auto-fill, minmax(240px, 1fr))';
  const REQUIRED_MARK = <span style={{
    color: '#d32f2f'
  }}>*</span>;
  const S = {
    card: {
      background: bg,
      border: '2px solid rgba(38,208,124,' + (isDark ? '0.4' : '0.5') + ')',
      borderRadius: '16px',
      padding: '1.25rem',
      color: text,
      boxShadow: '0 2px 8px rgba(38,208,124,0.08)'
    },
    group: {
      background: bgLight,
      borderRadius: '12px',
      padding: '0.75rem',
      border: '1px solid rgba(38,208,124,0.3)'
    },
    label: {
      display: 'block',
      fontWeight: 600,
      marginBottom: '0.25rem',
      color: text,
      fontSize: '0.8rem'
    },
    help: {
      fontSize: '0.7rem',
      color: muted,
      lineHeight: 1.3,
      marginTop: '0.25rem'
    },
    input: {
      padding: '0.5rem 0.75rem',
      border: BORDER,
      borderRadius: '8px',
      fontSize: '0.85rem',
      background: bg,
      color: text,
      width: '100%',
      boxSizing: 'border-box',
      outline: 'none'
    },
    select: {
      padding: '0.5rem 0.75rem',
      border: BORDER,
      borderRadius: '8px',
      fontSize: '0.85rem',
      background: bg,
      color: text,
      width: '100%',
      cursor: 'pointer',
      outline: 'none'
    },
    toggleBtn: (active, left) => ({
      flex: 1,
      padding: '0.5rem 0.75rem',
      border: 'none',
      borderLeft: left ? BORDER : 'none',
      cursor: 'pointer',
      fontSize: '0.85rem',
      fontWeight: 600,
      background: active ? GRAD : bgLight,
      color: active ? activeFg : text
    }),
    btn: primary => ({
      padding: '0.45rem 0.9rem',
      background: primary ? GRAD : secondaryBg,
      color: primary ? activeFg : text,
      border: '1.5px solid rgba(38,208,124,0.5)',
      borderRadius: '10px',
      cursor: 'pointer',
      fontSize: '0.8rem',
      fontWeight: 500
    }),
    fieldsetRow: {
      margin: 0,
      padding: 0,
      display: 'flex',
      borderRadius: '8px',
      overflow: 'hidden',
      border: BORDER
    },
    pre: {
      background: bg,
      color: text,
      padding: '0.75rem',
      borderRadius: '6px',
      fontFamily: "'Monaco','Menlo','Ubuntu Mono',monospace",
      fontSize: '0.78rem',
      lineHeight: 1.45,
      overflowX: 'auto',
      margin: 0,
      whiteSpace: 'pre',
      border: '1px solid rgba(38,208,124,0.15)',
      maxHeight: expanded ? 'none' : '320px',
      overflowY: expanded ? 'auto' : 'hidden'
    },
    notice: {
      marginTop: '0.5rem',
      padding: '0.5rem 0.75rem',
      background: isDark ? '#3b2f0b' : '#fff8e6',
      border: '1px solid ' + (isDark ? '#7a5c00' : '#f5d98a'),
      borderRadius: '8px',
      color: isDark ? '#f5d98a' : '#7a5c00',
      fontSize: '0.78rem',
      lineHeight: 1.45
    },
    err: {
      padding: '0.75rem',
      background: isDark ? '#3b1111' : '#fef2f2',
      border: '1px solid ' + (isDark ? '#7f1d1d' : '#fecaca'),
      borderRadius: '8px',
      color: isDark ? '#fecaca' : '#7f1d1d',
      fontSize: '0.82rem',
      lineHeight: 1.5
    },
    code: {
      background: bgLight,
      padding: '0.1rem 0.35rem',
      borderRadius: '4px',
      fontSize: '0.8em',
      fontFamily: "'Monaco','Menlo',monospace"
    }
  };
  let missingHint = 'Enter your API key, secret, and namespace to generate the config.';
  if (credsOk && useProfile && !profileOk) {
    missingHint = 'Enter a reverse-DNS profile identifier and organization to generate the .mobileconfig.';
  } else if (credsOk && invalidEnvKeys.length > 0) {
    missingHint = 'Fix the environment variable keys marked below to generate the config.';
  }
  const generationVisible = status === 'ready';
  const renderEnvEditor = () => <div style={{
    ...S.group,
    gridColumn: '1 / -1'
  }}>
      <span style={S.label}>Environment variables</span>
      <div style={S.help}>
        Seeded with <code style={S.code}>{CACHE_ENV_KEY}=true</code> (upstream default: the key is always present, only its value can change). Later rows with the same key replace earlier ones. Keys must match <code style={S.code}>[A-Za-z_][A-Za-z0-9_]*</code>.
      </div>
      <div style={{
    marginTop: '0.5rem',
    display: 'flex',
    flexWrap: 'wrap',
    gap: '0.4rem',
    alignItems: 'center'
  }}>
        <button type="button" aria-pressed={monitorOnlyOn} onClick={() => setMonitorOnly(!monitorOnlyOn)} style={S.btn(monitorOnlyOn)}>
          {monitorOnlyOn ? 'Monitor-only on' : 'Enable monitor-only'}
        </button>
        <span style={S.help}>
          Sets <code style={S.code}>{MONITOR_ENV_KEY}=true</code>. Actions are audited but not blocked.
        </span>
      </div>
      <div style={{
    marginTop: '0.6rem',
    display: 'flex',
    flexDirection: 'column',
    gap: '0.4rem'
  }}>
        {envs.map((row, index) => {
    const keyTrimmed = row.key.trim();
    const keyInvalid = keyTrimmed.length > 0 && !ENV_KEY_RE.test(keyTrimmed);
    return <div key={'env-' + index}>
              <div style={{
      display: 'flex',
      gap: '0.35rem',
      alignItems: 'center'
    }}>
                <input type="text" aria-label={'Env key ' + (index + 1)} aria-invalid={keyInvalid} value={row.key} onChange={e => updateEnvRow(index, 'key', e.target.value)} placeholder="KEY" readOnly={isSeededRow(row)} style={{
      ...S.input,
      flex: '1 1 40%',
      ...keyInvalid ? {
        borderColor: '#d32f2f'
      } : {}
    }} />
                <input type="text" aria-label={'Env value ' + (index + 1)} value={row.value} onChange={e => updateEnvRow(index, 'value', e.target.value)} placeholder="value" style={{
      ...S.input,
      flex: '1 1 40%'
    }} />
                {isSeededRow(row) ? <span style={{
      ...S.help,
      flexShrink: 0,
      marginTop: 0
    }}>always present</span> : <button type="button" onClick={() => removeEnvRow(index)} style={{
      ...S.btn(false),
      flexShrink: 0
    }} aria-label={'Remove env row ' + (index + 1)}>Remove</button>}
              </div>
              {keyInvalid && <div style={{
      ...S.help,
      color: '#d32f2f'
    }}>Invalid key: must start with a letter or underscore and contain only letters, digits, and underscores.</div>}
            </div>;
  })}
      </div>
      <button type="button" onClick={addEnvRow} style={{
    ...S.btn(false),
    marginTop: '0.5rem'
  }}>Add env</button>
    </div>;
  const renderProfileFields = () => {
    if (!profileAvailable) return null;
    return <div style={{
      ...S.group,
      gridColumn: '1 / -1'
    }}>
        <label style={{
      display: 'flex',
      alignItems: 'center',
      gap: '0.5rem',
      cursor: 'pointer'
    }}>
          <input type="checkbox" checked={wantProfile} onChange={e => setWantProfile(e.target.checked)} aria-label="Output as macOS MDM profile" />
          <span style={{
      ...S.label,
      marginBottom: 0
    }}>Output as macOS MDM profile (.mobileconfig)</span>
        </label>
        <div style={S.help}>
          {agent === 'codex' ? 'Codex on macOS only. Wraps the requirements.toml as an MCX Forced preference (' + MCX_DOMAIN_DEFAULT + ').' : 'Claude on macOS only. Wraps settings JSON in a Configuration Profile envelope.'}
        </div>
        {wantProfile && <div style={{
      display: 'grid',
      gridTemplateColumns: GRID,
      gap: '0.75rem',
      marginTop: '0.6rem'
    }}>
            <div>
              <span style={S.label}>Payload identifier {REQUIRED_MARK}</span>
              <input type="text" aria-label="Profile identifier" aria-required="true" value={profileIdentifier} onChange={e => setProfileIdentifier(e.target.value)} placeholder={'com.example.ai-governance.' + (agent === 'codex' ? 'codex' : 'claudecode')} style={S.input} />
              <div style={S.help}>Reverse-DNS identifier required by render-plist.sh.</div>
            </div>
            <div>
              <span style={S.label}>Organization {REQUIRED_MARK}</span>
              <input type="text" aria-label="Organization" aria-required="true" value={profileOrganization} onChange={e => setProfileOrganization(e.target.value)} placeholder="Acme Corp" style={S.input} />
            </div>
            <div>
              <span style={S.label}>Profile name</span>
              <input type="text" aria-label="Profile name" value={profileName} onChange={e => setProfileName(e.target.value)} style={S.input} />
              <div style={S.help}>Default: {PROFILE_NAME_DEFAULT}.</div>
            </div>
            {agent === 'claude' && <div>
                <span style={S.label}>Payload type</span>
                <input type="text" aria-label="Payload type" value={profilePayloadType} onChange={e => setProfilePayloadType(e.target.value)} style={S.input} />
                <div style={S.help}>Default: {PAYLOAD_TYPE_DEFAULT}.</div>
              </div>}
            <div>
              <span style={S.label}>Profile identifier override</span>
              <input type="text" aria-label="Profile identifier override" value={profileIdentifierOverride} onChange={e => setProfileIdentifierOverride(e.target.value)} placeholder="defaults to payload identifier" style={S.input} />
            </div>
          </div>}
      </div>;
  };
  return <div className="not-prose" style={{
    margin: '1.5rem 0',
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
  }}>
      <div style={S.card}>
        <div style={{
    marginBottom: '0.75rem'
  }}>
          <h3 style={{
    margin: '0 0 0.25rem 0',
    fontWeight: 600,
    fontSize: '1.1rem',
    color: text
  }}>Coding Agent Governance config generator</h3>
          <p style={{
    margin: 0,
    fontSize: '0.85rem',
    color: muted,
    lineHeight: 1.4
  }}>
            Generate Claude Code, Cursor, or Codex hook configs for MDM or local install. Assembly happens entirely in your browser. Credentials are never sent anywhere.
          </p>
        </div>

        {status === 'error' && <div style={S.err}>
            <strong>Couldn't read the generator sources from GitHub.</strong>
            {' '}Generate the config locally instead:
            <pre style={{
    ...S.pre,
    marginTop: '0.5rem'
  }}>{manualCmd}</pre>
            <div style={{
    marginTop: '0.5rem'
  }}>
              Source and instructions:{' '}
              <a href={UPSTREAM_URL} target="_blank" rel="noreferrer" style={{
    color: 'inherit',
    fontWeight: 600
  }}>
                github.com/endorlabs/mdm-scripts/agent-governance
              </a>
            </div>
          </div>}

        {generationVisible && <div style={{
    display: 'grid',
    gridTemplateColumns: GRID,
    gap: '0.75rem',
    marginBottom: '0.75rem'
  }}>
            <div style={S.group}>
              <span style={S.label} id="ag-agent">Agent</span>
              <fieldset aria-labelledby="ag-agent" style={S.fieldsetRow}>
                <button type="button" aria-pressed={agent === 'claude'} onClick={() => {
    setAgent('claude');
  }} style={S.toggleBtn(agent === 'claude', false)}>Claude Code</button>
                <button type="button" aria-pressed={agent === 'cursor'} onClick={() => {
    setAgent('cursor');
    setWantProfile(false);
  }} style={S.toggleBtn(agent === 'cursor', true)}>Cursor</button>
                <button type="button" aria-pressed={agent === 'codex'} onClick={() => {
    setAgent('codex');
  }} style={S.toggleBtn(agent === 'codex', true)}>Codex</button>
              </fieldset>
            </div>

            <div style={S.group}>
              <span style={S.label} id="ag-os">Target OS</span>
              <select aria-labelledby="ag-os" value={targetOS} onChange={e => {
    setTargetOS(e.target.value);
    if (e.target.value !== 'macos') setWantProfile(false);
  }} style={S.select}>
                <option value="macos">macOS</option>
                <option value="linux">Linux</option>
                <option value="windows">Windows</option>
              </select>
              <div style={S.help}>macOS and Linux produce identical POSIX hooks. Windows inlines encoded PowerShell.</div>
            </div>

            <div style={S.group}>
              <span style={S.label}>Namespace {REQUIRED_MARK}</span>
              <input type="text" aria-label="Namespace" aria-required="true" value={namespace} onChange={e => setNamespace(e.target.value)} placeholder="your-namespace" style={S.input} />
            </div>

            <div style={S.group}>
              <span style={S.label}>API URL</span>
              <input type="text" aria-label="API URL" value={apiUrl} onChange={e => setApiUrl(e.target.value)} style={S.input} />
              <div style={S.help}>Default: {DEFAULT_API_URL}</div>
            </div>

            <div style={S.group}>
              <span style={S.label}>API key {REQUIRED_MARK}</span>
              <input type="password" autoComplete="new-password" aria-label="API key" aria-required="true" value={apiKey} onChange={e => setApiKey(e.target.value)} style={S.input} />
              <div style={S.help}>Assembly is entirely in-browser. Credentials are never sent anywhere.</div>
            </div>

            <div style={S.group}>
              <span style={S.label}>API secret {REQUIRED_MARK}</span>
              <input type="password" autoComplete="new-password" aria-label="API secret" aria-required="true" value={apiSecret} onChange={e => setApiSecret(e.target.value)} style={S.input} />
              <div style={S.help}>Baked into the output in plaintext. Treat the downloaded file as a secret.</div>
            </div>

            <div style={{
    ...S.group,
    gridColumn: '1 / -1'
  }}>
              <label style={{
    display: 'flex',
    alignItems: 'center',
    gap: '0.5rem',
    cursor: 'pointer'
  }}>
                <input type="checkbox" checked={skipEndorctlUpdate} onChange={e => setSkipEndorctlUpdate(e.target.checked)} aria-label="Skip endorctl update" />
                <span style={{
    ...S.label,
    marginBottom: 0
  }}>Skip endorctl update on session start</span>
              </label>
              <div style={S.help}>Maps to <code style={S.code}>--skip-endorctl-update</code>. Useful once the fleet is provisioned.</div>
            </div>

            {renderEnvEditor()}
            {renderProfileFields()}
          </div>}

        {status === 'loading' && <div style={{
    ...S.group,
    color: muted,
    fontStyle: 'italic'
  }}>Loading source from GitHub…</div>}

        {generationVisible && !validInputs && <div style={{
    ...S.group,
    color: muted,
    fontSize: '0.82rem'
  }}>{missingHint}</div>}

        {generationVisible && validInputs && result && <div>
            <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '0.5rem',
    marginBottom: '0.4rem',
    flexWrap: 'wrap'
  }}>
              <span style={{
    fontWeight: 600,
    fontSize: '0.85rem',
    color: text
  }}>{meta.fileName}</span>
              <button type="button" onClick={handleCopy} style={{
    ...S.btn(true),
    marginLeft: 'auto'
  }}>{copied ? 'Copied' : 'Copy'}</button>
              <button type="button" onClick={handleDownload} style={S.btn(false)}>Download</button>
            </div>
            <div style={{
    ...S.help,
    marginBottom: '0.4rem'
  }}>{meta.pathHint}</div>
            <pre style={S.pre}>{result}</pre>
            <button type="button" onClick={() => setExpanded(v => !v)} style={{
    ...S.btn(false),
    marginTop: '0.4rem',
    width: '100%'
  }}>
              {expanded ? 'Show less' : 'Show full output'}
            </button>
            <div style={S.notice}>
              This config contains your API key and secret in plaintext (required for the agent hooks). Restrict access to the MDM policy and the downloaded file. Do not commit it.
            </div>
          </div>}

        {generationVisible && validInputs && assembleError && <div style={S.err}>
            <strong>Couldn't assemble the config.</strong> Generate it locally instead:
            <pre style={{
    ...S.pre,
    marginTop: '0.5rem'
  }}>{manualCmd}</pre>
          </div>}
      </div>
    </div>;
};

Coding Agent Governance configurations connect Cursor, Claude Code, and Codex events to Endor Labs. Use this page to generate hook configuration for individual machines or managed fleets.

You can create Cursor `hooks.json`, Claude Code `settings.json`, Codex `requirements.toml`, or a macOS configuration profile for Claude Code or Codex. The generated hooks install and update endorctl when a coding agent session starts.

## Before you begin

Complete the following tasks before you generate a configuration:

* Complete the [Coding Agent Governance prerequisites](/agent-governance/prerequisites).
* Create an API key with the **AI Audit User** role.
* Identify the namespace that owns your Coding Agent Governance policies.
* For manual generation, install `git` on the admin machine. The scripts run with system tools only. Creating a macOS configuration profile also requires macOS and `plutil`.

## Generate a configuration in your browser

The browser generator assembles the configuration locally. Your API key, secret, namespace, and environment variable values are never sent to Endor Labs or GitHub.

Use the manual generation steps below if the browser generator isn't available or can't fetch its source files.

<AgentGovernanceConfigGenerator />

## Generate a configuration manually

The [Endor Labs MDM scripts repository](https://github.com/endorlabs/mdm-scripts/tree/main/agent-governance) is the authoritative source for the generator scripts. Clone the repository on an admin machine.

```bash theme={null}
git clone https://github.com/endorlabs/mdm-scripts
cd mdm-scripts/agent-governance
```

Set your credentials as environment variables. The generator reads these exact variable names, and prompts for any missing credential when you run it interactively.

```bash theme={null}
export ENDOR_API_CREDENTIALS_KEY="<api-key>"
export ENDOR_API_CREDENTIALS_SECRET="<api-secret>"
export ENDOR_NAMESPACE="<namespace>"
```

Generate the configuration that matches your coding agent and deployment method.

<Tabs>
  <Tab title="Cursor">
    Generate `hooks.json` for macOS or Linux.

    ```bash theme={null}
    scripts/render.sh --agent cursor \
      --api-key "$ENDOR_API_CREDENTIALS_KEY" \
      --api-secret "$ENDOR_API_CREDENTIALS_SECRET" \
      --namespace "$ENDOR_NAMESPACE" \
      -o cursor-hooks.json
    ```

    For Windows, add `--target-os windows`. The generated hook uses an encoded PowerShell command that can run from Git Bash, PowerShell, or Command Prompt.

    ```bash theme={null}
    scripts/render.sh --agent cursor --target-os windows \
      --api-key "$ENDOR_API_CREDENTIALS_KEY" \
      --api-secret "$ENDOR_API_CREDENTIALS_SECRET" \
      --namespace "$ENDOR_NAMESPACE" \
      -o cursor-hooks.json
    ```
  </Tab>

  <Tab title="Claude Code">
    Generate `settings.json` for a user installation, Linux managed settings, or Windows deployment.

    ```bash theme={null}
    scripts/render.sh --agent claude \
      --api-key "$ENDOR_API_CREDENTIALS_KEY" \
      --api-secret "$ENDOR_API_CREDENTIALS_SECRET" \
      --namespace "$ENDOR_NAMESPACE" \
      -o claude-settings.json
    ```

    For Windows, add `--target-os windows`.

    ```bash theme={null}
    scripts/render.sh --agent claude --target-os windows \
      --api-key "$ENDOR_API_CREDENTIALS_KEY" \
      --api-secret "$ENDOR_API_CREDENTIALS_SECRET" \
      --namespace "$ENDOR_NAMESPACE" \
      -o claude-settings.json
    ```
  </Tab>

  <Tab title="Claude Code macOS profile">
    Pipe the Claude Code settings through `render-plist.sh` to create a `.mobileconfig` file. Replace the identifier and organization with values for your organization.

    ```bash theme={null}
    scripts/render.sh --agent claude \
      --api-key "$ENDOR_API_CREDENTIALS_KEY" \
      --api-secret "$ENDOR_API_CREDENTIALS_SECRET" \
      --namespace "$ENDOR_NAMESPACE" \
      -o - \
    | scripts/render-plist.sh \
      --identifier com.example.ai-governance.claudecode \
      --organization "Example Organization" \
      --name "Claude Code - Endor AI Governance" \
      -o com.anthropic.claudecode.mobileconfig
    ```
  </Tab>

  <Tab title="Codex">
    Generate `requirements.toml` with managed hooks for macOS or Linux. Hooks from a managed requirements source are trusted by policy and run without per-user approval. Codex has no managed environment block, so the credentials are baked into every hook command. Treat the file as a secret.

    ```bash theme={null}
    scripts/render.sh --agent codex \
      --api-key "$ENDOR_API_CREDENTIALS_KEY" \
      --api-secret "$ENDOR_API_CREDENTIALS_SECRET" \
      --namespace "$ENDOR_NAMESPACE" \
      -o codex-requirements.toml
    ```

    For Windows, add `--target-os windows`.
  </Tab>

  <Tab title="Codex macOS profile">
    Pipe the Codex requirements through `render-plist.sh --style mcx` to create a `.mobileconfig` file that delivers the TOML as a managed preference. Replace the identifier and organization with values for your organization.

    ```bash theme={null}
    scripts/render.sh --agent codex \
      --api-key "$ENDOR_API_CREDENTIALS_KEY" \
      --api-secret "$ENDOR_API_CREDENTIALS_SECRET" \
      --namespace "$ENDOR_NAMESPACE" \
      -o - \
    | scripts/render-plist.sh --style mcx \
      --identifier com.example.ai-governance.codex \
      --organization "Example Organization" \
      --name "Codex - Endor AI Governance" \
      -o com.openai.codex.mobileconfig
    ```
  </Tab>
</Tabs>

<Warning>
  The generated configuration contains your API key and secret. Treat the output and its MDM policy as secrets. Restrict access and rotate the API key if someone exposes the configuration.
</Warning>

### Configure optional behavior

The generator enables the local response cache by default. Add `--env ENDOR_AI_AUDIT_NO_BLOCKING=true` to evaluate and record actions without blocking them.

```bash theme={null}
scripts/render.sh --agent cursor \
  --api-key "$ENDOR_API_CREDENTIALS_KEY" \
  --api-secret "$ENDOR_API_CREDENTIALS_SECRET" \
  --namespace "$ENDOR_NAMESPACE" \
  --env ENDOR_AI_AUDIT_NO_BLOCKING=true \
  -o cursor-hooks.json
```

Add `--skip-endorctl-update` if your fleet provisions endorctl separately and shouldn't check for updates when a session starts.

## Deploy the generated configuration

Deliver the generated file through your mobile device management (MDM) or configuration management tool.

### Deploy Cursor hooks

Deliver `cursor-hooks.json` to the location that matches your deployment:

* For a local trial, copy `cursor-hooks.json` to `~/.cursor/hooks.json`.
* On managed macOS machines, deliver it to `/Library/Application Support/Cursor/hooks.json`.
* On managed Linux machines, deliver it to `/etc/cursor/hooks.json`.
* On Windows, push `cursor-hooks.json` with your MDM tool.

See [Deploy hooks for Cursor](/agent-governance/cursor) to verify the installation and review the registered hook events.

### Deploy Claude Code settings

Deliver the generated Claude Code configuration to the location that matches your deployment:

* For a local trial, copy `claude-settings.json` to `~/.claude/settings.json`.
* On managed Linux machines, deliver it to `/etc/claude-code/managed-settings.json`.
* On Windows, push `claude-settings.json` with your MDM tool.
* On managed macOS machines, upload `com.anthropic.claudecode.mobileconfig` as a custom profile.

See [Deploy hooks for Claude Code](/agent-governance/claude-code) to verify the installation and review the registered hook events.

### Deploy Codex requirements

Deliver the generated Codex configuration to the location that matches your deployment:

* On managed macOS and Linux machines, deliver `codex-requirements.toml` to `/etc/codex/requirements.toml`.
* On managed macOS machines, you can instead upload `com.openai.codex.mobileconfig` as a custom profile.
* On Windows, push `codex-requirements.toml` to `%ProgramData%\OpenAI\Codex\requirements.toml` with your MDM tool.

See [Deploy hooks for Codex](/agent-governance/codex) to verify the installation and review the registered hook events.

## Update a deployment

The session hook installs endorctl on first use and checks for updates during later session starts. It verifies the downloaded binary with SHA-256.

Regenerate and redeploy the configuration when you change credentials, environment variables, or delivery settings. Server-side Coding Agent Governance policy changes don't require a new configuration.

## Keep a fleet current with the scheduled runner

The [mdm-scripts repository](https://github.com/endorlabs/mdm-scripts/tree/main/agent-governance) also ships `scripts/runner.sh`, which replaces one-shot generation with a self-updating deployment on macOS and Linux. Paste it into your MDM as a recurring script body (a Jamf script, Kandji Custom Script, or JumpCloud Command) and edit the settings block at the top to pick the agent and pin a reviewed revision. On each run it fetches the repository at that revision, renders the configuration on the endpoint, and replaces the installed file atomically, only when the result changed. Repository, credential, and flag changes then roll out on the next scheduled run without a redeploy.

Keep the following in mind:

* The MDM must set `ENDOR_API_CREDENTIALS_KEY`, `ENDOR_API_CREDENTIALS_SECRET`, and `ENDOR_NAMESPACE` in the script's environment. Use an API key with the **AI Audit User** role.
* Endpoints need only `curl` and `tar`, which ship with macOS and Linux.
* Windows does not use the runner. Pre-generate the configuration and push it with Intune or your MDM tool.
