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

# How Coding Agent Governance works

> Understand how hook events from your AI coding agents flow into the Endor Labs inventory, policies, and overview.

export const Diagram = ({children}) => {
  const [svg, setSvg] = useState('');
  const [error, setError] = useState(null);
  const [mounted, setMounted] = useState(false);
  const [id] = useState(() => `diagram-${Math.random().toString(36).slice(2)}`);
  const DEFAULTS = {
    theme: 'base',
    fontSize: '14px',
    fontFamily: 'inherit',
    primaryColor: '#26D07C',
    primaryTextColor: '#000000',
    primaryBorderColor: '#059669',
    secondaryColor: '#e5e7eb',
    secondaryTextColor: '#000000',
    secondaryBorderColor: '#9ca3af',
    tertiaryColor: '#e5e7eb',
    tertiaryTextColor: '#000000',
    tertiaryBorderColor: '#9ca3af',
    lineColor: '#6b7280',
    background: '#ffffff',
    edgeLabelBackground: '#f9fafb',
    clusterBkg: '#f0fdf4',
    clusterBorder: '#059669',
    nodeTextColor: '#000000',
    endorColor: '#26D07C',
    endorBorder: '#059669',
    managedColor: '#A7F3D0',
    managedBorder: '#059669',
    externalColor: '#e5e7eb',
    externalBorder: '#9ca3af'
  };
  const VAR_LINE_RE = /^(\w+):\s*(.+)$/;
  const TOKEN_RE = /\{\{(\w+)\}\}/g;
  const parseVarsBlock = raw => {
    const vars = {};
    const lines = raw.trim().split('\n');
    const diagramLines = [];
    let inVars = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed === '%%vars') {
        inVars = true;
        continue;
      }
      if (inVars && trimmed === '%%') {
        inVars = false;
        continue;
      }
      if (inVars) {
        const m = VAR_LINE_RE.exec(trimmed);
        if (m) vars[m[1]] = m[2];
      } else {
        diagramLines.push(line);
      }
    }
    return {
      vars,
      diagramLines
    };
  };
  const buildInitConfig = merged => ({
    theme: merged.theme,
    themeVariables: {
      fontSize: merged.fontSize,
      fontFamily: merged.fontFamily,
      primaryColor: merged.primaryColor,
      primaryTextColor: merged.primaryTextColor,
      primaryBorderColor: merged.primaryBorderColor,
      secondaryColor: merged.secondaryColor,
      secondaryTextColor: merged.secondaryTextColor,
      secondaryBorderColor: merged.secondaryBorderColor,
      tertiaryColor: merged.tertiaryColor,
      tertiaryTextColor: merged.tertiaryTextColor,
      tertiaryBorderColor: merged.tertiaryBorderColor,
      lineColor: merged.lineColor,
      background: merged.background,
      edgeLabelBackground: merged.edgeLabelBackground,
      clusterBkg: merged.clusterBkg,
      clusterBorder: merged.clusterBorder,
      nodeTextColor: merged.nodeTextColor
    }
  });
  const renderWithMermaid = (mermaid, fullDiagram) => {
    mermaid.initialize({
      startOnLoad: false,
      zoom: {
        enabled: false
      }
    });
    mermaid.render(id, fullDiagram).then(({svg: rendered}) => {
      setSvg(rendered);
      setError(null);
    }).catch(err => setError(err.message));
  };
  useEffect(() => {
    setMounted(true);
  }, []);
  useEffect(() => {
    if (!mounted || !children) return;
    const raw = typeof children === 'string' ? children : String(children);
    const {vars, diagramLines} = parseVarsBlock(raw);
    const merged = {
      ...DEFAULTS,
      ...vars
    };
    const diagram = diagramLines.join('\n').trim().replaceAll(TOKEN_RE, (_, key) => merged[key] || '');
    const fullDiagram = `%%{init: ${JSON.stringify(buildInitConfig(merged))}}%%\n${diagram}`;
    const existing = document.getElementById(id);
    if (existing) existing.remove();
    if (globalThis.mermaid) {
      renderWithMermaid(globalThis.mermaid, fullDiagram);
    } else {
      const script = document.createElement('script');
      script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
      script.onload = () => renderWithMermaid(globalThis.mermaid, fullDiagram);
      script.onerror = () => setError('Failed to load Mermaid');
      document.head.appendChild(script);
    }
  }, [mounted, children, id]);
  if (!mounted) return null;
  if (error) {
    return <pre style={{
      color: '#dc2626',
      background: '#fef2f2',
      padding: '12px',
      borderRadius: '6px',
      fontSize: '13px',
      overflowX: 'auto'
    }}>
        Diagram error: {error}
      </pre>;
  }
  if (!svg) {
    return <div style={{
      height: '200px',
      background: '#f3f4f6',
      borderRadius: '8px',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      color: '#9ca3af',
      fontSize: '14px'
    }}>
        Loading diagram...
      </div>;
  }
  return <div dangerouslySetInnerHTML={{
    __html: svg
  }} style={{
    overflowX: 'auto',
    padding: '8px 0'
  }} />;
};

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>;
};

Coding Agent Governance gives security and engineering teams visibility and control over how AI coding agents work inside your organization. Endor Labs collects events from agents, builds an inventory of your developers' AI use, and applies your policies to risky activity in real time.

Use Coding Agent Governance to answer questions such as:

* Which AI coding agents and MCP servers are running on developer machines?
* Are agents reading sensitive files, executing dangerous commands, or talking to unapproved MCP servers?
* When a policy fires, what was the developer trying to do, and which agent was responsible?

Coding Agent Governance has the following components:

* Hooks that run on developer machines and capture events
* Coding Agent Governance policies that govern these events
* Dashboards that show what your agents are doing

Together, these components let you control and view the activities of AI agents across your organization.

## What Coding Agent Governance is (and isn't)

Coding Agent Governance puts guardrails around what coding agents do so it can catch accidental or unintended actions before they cause damage. It reduces the chance an agent does something it shouldn't while working through a task.

It is not a sandbox, firewall, or hardened security boundary. Policy controls lower risk and blast radius, but they do not guarantee that a risky action is impossible. Hard guarantees require isolation at the operating system, system call, or sandbox level. Coding Agent Governance works alongside that kind of isolation. It does not replace it.

Coding Agent Governance targets accidental agent behavior. It is not designed to stop a determined user who deliberately tries to defeat the controls.

## Lifecycle of an event

When a developer starts a session, an Endor Labs hook runs alongside the agent and emits a normalized event for every meaningful action. Each event carries the agent type, the user, the workspace context, and the tool, command, or file the agent acted on.

The event takes two paths in parallel:

* Local evaluation: The hook checks the event against a cached copy of Coding Agent Governance policies. If a policy matches, the hook can stop the action, alert the user, or pause for the developer to approve before the agent proceeds.
* Centralized aggregation: The hook forwards the event to Endor Labs, where it updates the inventory, the Coding Agent Governance overview, and the policy violation log.

Together, these methods enforce policies on developer machines in real time and give your security team a single place to monitor activity and plan their response. As your AI agent inventory evolves, your security team can refine policies and roll out changes.

The following diagram shows the data flow in Coding Agent Governance.

<Diagram>
  {`
    flowchart TD
      subgraph Dev[Developer machine]
          A[Coding agent action] --> B[Endor Labs hook]
          B --> C[Local policy evaluation]
          C --> D[Block / Alert / Ask Permission / Allow]
      end
      subgraph Endor[Endor Labs]
          E[Inventory]
          F[Overview]
          G[Policy Violations]
      end
      B -->|Event metadata| E
      B -->|Event metadata| F
      B -->|Event metadata| G
    `}
</Diagram>

## Hooks and what they govern

A hook is a small script Endor Labs deploys to your developers' machines. It runs whenever the agent starts a session, submits a user prompt, calls an MCP tool, runs a shell command, or reads, writes, or deletes a file. For each event, the hook can apply a policy locally before the agent acts.

Hooks enforce guardrails such as blocking dangerous commands or gating MCP tool use. They are instruction-based controls that the agent and host honor, not a tamper-proof runtime.

You can write policies for the following activities:

* Shell commands the agent tries to run, matched by regex.
* File reads, writes, deletes, and edits, matched by regex on the file path.
* MCP server and tool calls, matched by regex on server name and tool name.
* Package installs, checked against the Endor Labs malware feed.
* File content the agent writes or reads, scanned for secrets.

The malware and secrets checks carry no authored patterns. Their verdict comes from Endor Labs data. See [Secure coding checks](/agent-governance/secure-coding).

When a policy matches, the hook applies the action you chose:

* **Block** stops the action and returns a deny response. The response tells the agent the action was denied by a governance policy and not to work around the block.
* **Alert** logs the event without stopping the agent.
* **Ask Permission** pauses for the developer to approve or deny.

See [Write a policy](/agent-governance/policies) for allowlists, blocklists, and how to override in-built system policies.

## Example: a blocked malicious package install

The following session shows the lifecycle end to end. A developer asks a coding agent to install an npm package, the hook checks the install against a malware policy on the developer's machine, and the block stops the install before any package code runs.

```
> Run this command in non-interactive mode: npm install plugin-proposal-json-strings

  I'll check what this package actually is before installing. The name looks like
  an unscoped variant of @babel/plugin-proposal-json-strings.

  Bash(npm view plugin-proposal-json-strings --json)
    The only remaining version is 0.0.1-security, described as a "security holding
    package" from npm/security-holder. The purged version history (99.10.9, 9.9.9,
    8.0.11) is a dependency-confusion pattern.

  Bash(npm install plugin-proposal-json-strings)
    Error: [ENDOR LABS SECURITY POLICY BLOCK] This action was identified as an
    install of a known-malicious or untrusted software package. malware detected in
    npm package "plugin-proposal-json-strings" (flagged by MALWARE_SOURCE_OSV).
    Endor Labs flagged this package as known-malicious. Do not install the flagged
    package. If the dependency is genuinely needed, choose a non-flagged, safe
    version or a different well-known package, or ask the user. Do not try to
    install the same package by another route (a different registry, a direct
    download, renaming, or --force) or by disabling this check — that is treated as
    an evasion.
    Tell the user this was blocked by the Endor Labs policy "Malware: block
    installs of known-malicious packages".

  The install was blocked, and I'm not going to attempt it another way. npm install
  plugin-proposal-json-strings was blocked by the Endor Labs policy "Malware: block
  installs of known-malicious packages". The package was flagged as known-malicious
  (MALWARE_SOURCE_OSV). The package you probably want is the scoped Babel one,
  @babel/plugin-proposal-json-strings.
```

The example shows four things about how enforcement behaves:

* The hook evaluated the install at the moment the agent ran it, so no package code executed. Nothing depended on the agent choosing to check the package first.
* The verdict came from the Endor Labs malware feed rather than a pattern someone authored. See [Malware check](/agent-governance/secure-coding#malware-check).
* The deny response tells the agent what category of action was denied, gives the evidence and the feed source, names the policy that fired, and instructs the agent not to route around the block or disable the check. The agent stopped and reported the block instead of retrying with a different registry or `--force`.
* The event reached **Policy Violations** with the package, ecosystem, and feed source alongside the agent and user, so your security team sees the attempt even though nothing was installed.

## Data sent to Endor Labs

Each hook event carries the metadata Endor Labs needs for inventory and policy evaluation:

* Agent name, version, and (where available) model
* Session and sub-session IDs
* Device (endpoint) identifier
* Session configuration, such as permission mode and sandbox state
* User identifier when the agent provides one
* Repository name (not populated for Claude Code sessions) and working directory
* MCP server name, tool name, and call counts
* Shell command line (regex-matched against your policies)
* File path and operation type (read, write, delete, edit)
* Secrets check match (rule, line, column, and fingerprint, never the secret value)
* Policy match outcome and the action applied

### Secret redaction

Endor Labs runs every command line, file path, URL, environment value, and MCP tool input or output through a regex-based redactor before it leaves the developer's machine. Environment variables and keys named like a secret (such as `TOKEN`, `SECRET`, `API_KEY`, `PASSWORD`, or `AUTHORIZATION`) become `[REDACTED]`. The redactor also matches common secret formats in place: URL and database-connection credentials, AWS access keys, JWTs, PEM private keys, and provider tokens such as GitHub (`ghp_*`), OpenAI, and Anthropic keys.

Redaction of event payloads is best-effort regex pattern matching. A secret that does not match a known pattern or key name can still reach the backend. The [secrets check](/agent-governance/secure-coding) is separate from redaction: it detects secrets in file content on the developer's machine, and its violations carry the rule and location, never the value.

<Warning>
  On Cursor, the `sessionStart` hook returns the Endor Labs credential values to Cursor in plaintext under an `AGENT_HOOK_ENDOR_*` prefix, so subsequent hooks in the session inherit them. The values never leave the developer's machine, but anything that captures Cursor hook stdout (such as debug logging or screen recording) can see them.
</Warning>

The hook writes two Coding Agent Governance files to a developer's disk. The hook refreshes the local policy cache (`~/.endorctl/aigovernance/endor-policies.jsonl`) at session start. When the event cache is enabled, as the recommended Cursor and Claude Code configurations do, the hook also queues events in `~/.endorctl/aigovernance/localdb/cache-<namespace>.db` and flushes them in batches within about 30 seconds of hook activity, and fully at session start and end.

## What you see in Endor Labs

After you deploy hooks and at least one policy exists, select **Agent Governance** from the left sidebar:

<YamlTable>
  {`
    - Sub-drawer entry: Overview
    What it shows: Counts of developers using AI, distinct coding agents in use, coding agent sessions, blocked MCP servers, and policy violations over a chosen time range.
    - Sub-drawer entry: Workstation Inventory
    What it shows: Each agent your developers run, with versions reported, total sessions, distinct users, and last activity.
    - Sub-drawer entry: MCP Servers
    What it shows: MCP servers your agents called, with host, type, call count, distinct tools, and last-seen activity.
    - Sub-drawer entry: AI Models
    What it shows: The models invoked through agents, with session and user counts.
    - Sub-drawer entry: Skills
    What it shows: The skills your agents loaded, with the agents that used each skill and an Endor Score.
    - Sub-drawer entry: Policy Violations
    What it shows: A filterable feed of every event that matched a policy, with the agent, the user, and the action Endor Labs took.
    `}
</YamlTable>

Policies live with the rest of your platform policies. To open them, select **Policies & Rules** from the left sidebar, then select **Agent Governance**.

## Limitations of Coding Agent Governance

Coding Agent Governance currently has the following limitations.

### Platform and agent coverage

Coverage has the following boundaries:

* Hooks run on macOS, Linux, and Windows developer machines. CI runners are not supported. See [Supported agents and platforms](/agent-governance#supported-agents-and-platforms) for the full matrix.
* Policy enforcement covers Cursor, Claude Code, Codex, and GitHub Copilot. Other AI tools (such as Continue, Cody, Gemini CLI, and Amazon Q) are still discovered at session start. They appear in the **Workstation Inventory** table with a **Discovered • Not governed** status, and no hook fires for them.

### Enforcement behavior

Keep the following enforcement behaviors in mind:

* Policy edits take effect at the next session start on a developer's machine, when the hook refreshes the local policy cache. Tell developers to restart the agent to pick up policy changes.
* Enforcement hooks fail open. If the hook cannot run, or a malware policy cannot reach the Endor Labs API for a verdict, the action proceeds. An unreadable local policy cache does not disable enforcement, because the hook falls back to the built-in system policies. When the hook cannot run, the agent and the IDE keep working, but the event is missing from inventory and policy violations until the cause is fixed.
* In Cursor, **Ask Permission** pauses for approval on shell commands and MCP tool calls. It does not pause on file reads, where an **Ask** policy proceeds and is recorded, the same way **Alert** behaves.
* In Codex, **Ask Permission** cannot pause a tool call, so it fails closed and denies the call with the ask message. At Codex's own approval prompts, an **Ask** policy defers to that prompt instead. See [Deploy hooks for Codex](/agent-governance/codex).
* In GitHub Copilot, **Ask Permission** prompts in VS Code agent mode only. The Copilot CLI honors deny but not ask, so an **Ask** policy on a CLI session proceeds and lands under **Policy Violations**, the same way **Alert** behaves. See [Deploy hooks for GitHub Copilot](/agent-governance/copilot).
* Policies match commands and patterns, not the end effect. An agent can occasionally reach the same result another way. That trade-off comes with best-effort, pattern-based controls.

### Attribution and visibility

Attribution has the following gaps:

* User attribution is populated when the agent's hook payload includes a user identifier. Cursor supplies one on most events. Claude Code, Codex, and GitHub Copilot attempt to derive one from the local environment and may succeed depending on the version and configuration, but the value can still be empty. The **User** column on **Policy Violations** and the user count on the **Overview** stay empty for events without an identifier.
* endorctl captures device (endpoint) attribution on every event, but Coding Agent Governance does not surface it in the user interface. You cannot filter or scope inventory or policy violations by device.
* Claude Code sub-agent activity may carry no user identity, and the hook does not record sub-agent MCP tool calls that a policy cleanly allows. Policies still evaluate and enforce on sub-agent activity.

### Inventory and scoring

Inventory and scoring have the following limits:

* Endor Labs computes MCP server risk scores only for servers it finds in an agent's configuration files. A server invoked without a discoverable configuration entry can appear in inventory from its tool calls without a score.
* Endor Labs discovers MCP servers that enabled Claude Code plugins contribute, from the installed-plugin registry (`~/.claude/plugins`) and from skills-directory plugins. Each server appears in inventory under the name Endor Labs derives from its runtime (the command binary or URL host), falling back to `<plugin>/<server>` when it cannot derive one, attributed to the level that installed the plugin. Endor Labs does not inventory disabled plugins or marketplace plugins that are not installed locally.
* Endor Labs does not assess agent plugins as a single unit. It evaluates the MCP servers and skills a plugin contributes, not the plugin itself.
* File access matching is regex-based on path. The [secrets check](/agent-governance/secure-coding#secrets-check) adds content-aware secret detection, but Endor Labs does not redact or rewrite the content an agent reads.
* Skills inventory reflects skills discovered in the workspace and user-home directories at session start. Skills loaded at runtime by the agent, or installed outside those directories, might not appear. See [Skills](/agent-governance/inventory#skills) for the exact paths scanned.
* Hooks capture session configuration such as permission mode and sandbox state. Use [Session Posture](/agent-governance/policies#session-posture) policies to match sessions by sandbox setting. Richer client-posture context in the inventory is limited today.
