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

# Review your agent inventory

> Find the AI coding agents, MCP servers, models, and skills your developers use, with last-seen activity for each.

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

The Coding Agent Governance inventory is a live picture of what your developers run on a workstation. Each entry aggregates events from the deployed hooks so you can answer the question, "What AI is in my organization right now?"

## Open the inventory

Select **Agent Governance** from the left sidebar, then choose what you want to review:

* **Workstation Inventory** to see installed agents and their activity.
* **MCP Servers** to see the MCP servers your agents call.
* **AI Models** to see the underlying models invoked through agents.
* **Skills** to see the skills your agents loaded during sessions.

Each entry lets you filter, search, and pivot into the Coding Agent Governance overview, policies, or policy violations.

## Workstation Inventory

Selecting **Workstation Inventory** shows every agent and AI tool your hooks have observed. Above the table, three summaries provide context: a **Coding Agents** pie that breaks down active agents, a **Sessions This Week** trend, and a **Developers with AI** counter.

<img src="https://mintcdn.com/endorlabs-b4795f4f/dPDQPDyDLWVGxIZu/images/agent-governance/workstation-inventory.webp?fit=max&auto=format&n=dPDQPDyDLWVGxIZu&q=85&s=cf98984393fa0f001ad0334cb541814a" alt="Workstation Inventory" width="1200" height="900" data-path="images/agent-governance/workstation-inventory.webp" />

<YamlTable>
  {`
    - Column: **Name**
    What it shows: The agent or tool name, with an adoption status icon beside it: a green dot for **Approved** (governed) agents, or an orange triangle for **Discovered • Not governed** tools your hooks observed but have not adopted.
    - Column: **Type**
    What it shows: The agent or tool category: **Coding Agent**, **IDE**, **CLI Tool**, **AI Agent**, **Framework**, **MCP Config**, or **IDE Extension**. Rows without a recognized category show **Unspecified**.
    - Column: **Versions**
    What it shows: Every version the hooks reported, grouped together.
    - Column: **Sessions**
    What it shows: Total agent sessions captured.
    - Column: **Users**
    What it shows: Distinct developers seen using the agent or tool.
    - Column: **Last Active**
    What it shows: When the agent or tool last sent an event.
    `}
</YamlTable>

Branded logos appear only for governed agents such as Cursor, Claude Code, Codex, and GitHub Copilot. Every other row (including discovered tools and any agent type the user interface has not yet learned to recognize) shows a generic AI model icon.

Use this to spot agents that are out of date, developers whose agent has stopped reporting, and tools your developers added that you have not adopted.

Select a row to open the agent's detail drawer. The header shows a **Governed** or **Discovered** status chip, followed by **Sessions**, **Distinct Users**, and **Last Seen** cards, the reported version, and an **Activity** section. Under **Activity**, select **Recent Events** to open the event list in a side panel, or **Policy Violations** to jump to the violations for that agent.

<img src="https://mintcdn.com/endorlabs-b4795f4f/dPDQPDyDLWVGxIZu/images/agent-governance/workstation-inventory-drawer.webp?fit=max&auto=format&n=dPDQPDyDLWVGxIZu&q=85&s=af11a19fcd04e759da06beed1f907e9a" alt="Workstation Inventory with the Cursor agent detail drawer open, showing the Governed status chip, usage cards, version, and Activity section" width="2400" height="1800" data-path="images/agent-governance/workstation-inventory-drawer.webp" />

## MCP Servers

Selecting **MCP Servers** shows every MCP server your agents have called. Three charts summarize them above the table: **Local vs. Remote** by where the server runs, **Approved & Blocked** by your review decision, and **Suspicious & Malicious** by threat classification. Use the **Risk** filter to focus on the riskiest servers.

Policies set the review and threat labels. A policy with an inventory classification tags the servers it matches as **Approved**, **Blocked**, **Suspicious**, or **Malicious**, and servers with no matching policy show **Unreviewed**. See [Inventory classification](/agent-governance/policies#inventory-classification) to set the tags.

The label and the score are different signals. The label records your organization's decision, applied by your policies. The [Endor Score](/agent-governance/endor-score) and the risk band derived from it are computed by Endor Labs from the server's configuration and source. A low-scoring server stays **Unreviewed** until a policy classifies it. To connect the two, write an [MCP Server Posture](/agent-governance/policies#mcp-server-posture) policy that matches on a score threshold and carries an inventory classification. For example, tag every server scoring 4 or lower as **Blocked**.

<img src="https://mintcdn.com/endorlabs-b4795f4f/dPDQPDyDLWVGxIZu/images/agent-governance/mcp-servers-inventory.webp?fit=max&auto=format&n=dPDQPDyDLWVGxIZu&q=85&s=eac5842018cf23783073f64d9ae2fed6" alt="MCP Servers inventory" width="1200" height="900" data-path="images/agent-governance/mcp-servers-inventory.webp" />

<YamlTable>
  {`
    - Column: **MCP Server Name**
    What it shows: The server identifier reported by the agent.
    - Column: **Host**
    What it shows: Where the server runs.
    - Column: **Endor Score**
    What it shows: The server's trust score, higher is safer. See [Endor Score](/agent-governance/endor-score) for how it is computed.
    - Column: **Type**
    What it shows: Whether the server runs locally or remotely.
    - Column: **Calls**
    What it shows: The number of MCP tool calls Endor Labs has captured.
    - Column: **Tools**
    What it shows: The distinct tools your agents called on this server.
    - Column: **Last Active**
    What it shows: When the server last received a call.
    `}
</YamlTable>

Use this to identify unsanctioned MCP servers. Select a server to open its detail drawer: **Tool Calls**, **Distinct Users**, and **Last Seen** cards, a details section with the server type, host, transport, launch command, available tools, and environment variable names, and a **Risk** section. Under **Risk**, the **Policy Violations** count links to the violations for that server, and **Endor Score Factors** opens the per-dimension [Endor Score](/agent-governance/endor-score) breakdown in a side panel. See [Write a policy](/agent-governance/policies) to gate access to specific servers.

<img src="https://mintcdn.com/endorlabs-b4795f4f/dPDQPDyDLWVGxIZu/images/agent-governance/mcp-server-drawer.webp?fit=max&auto=format&n=dPDQPDyDLWVGxIZu&q=85&s=2671d115d7dc908e68437dc5e38fdda6" alt="MCP server detail drawer showing usage cards, the server details section, and the Risk section with the Endor Score Factors entry" width="2400" height="1800" data-path="images/agent-governance/mcp-server-drawer.webp" />

Endor Labs doesn't score agent plugins as a single unit. It evaluates the MCP servers and skills a plugin contributes. See [How Coding Agent Governance works](/agent-governance/how-it-works#inventory-and-scoring) for related inventory limits.

## AI Models

Selecting **AI Models** shows the language models invoked through your agents. Above the table, an **AI Models by Provider** pie breaks down model use by provider, and a **Top Model Sessions** list ranks the most-used models in the selected time range. Every column in the table is sortable.

<img src="https://mintcdn.com/endorlabs-b4795f4f/dPDQPDyDLWVGxIZu/images/agent-governance/ai-models-inventory.webp?fit=max&auto=format&n=dPDQPDyDLWVGxIZu&q=85&s=8e9019ad29f26cca3f393a9f25716954" alt="AI Models inventory" width="1200" height="900" data-path="images/agent-governance/ai-models-inventory.webp" />

<YamlTable>
  {`
    - Column: **Model**
    What it shows: The model name reported in agent telemetry.
    - Column: **Sessions**
    What it shows: Total agent sessions in which the model was used.
    - Column: **Users**
    What it shows: Distinct developers who used the model.
    - Column: **Last Active**
    What it shows: When the model last appeared in an event.
    `}
</YamlTable>

Use this to track model adoption and to flag models that fall outside your approved set.

## Skills

Selecting **Skills** shows the skills your agents loaded during sessions. Three charts summarize them above the table: **Approved & Blocked** by your review decision, **Suspicious & Malicious** by threat classification, and **Top 5 Riskiest Skills** by Endor Score. Use the **Risk** filter to focus on the riskiest skills.

As with MCP servers, policies set the review and threat labels. See [Inventory classification](/agent-governance/policies#inventory-classification). These labels are separate from the score bands (**Unauthorized**, **Caution**, **Safe**) that come from the [Endor Score](/agent-governance/endor-score).

<img src="https://mintcdn.com/endorlabs-b4795f4f/dPDQPDyDLWVGxIZu/images/agent-governance/skills-inventory.webp?fit=max&auto=format&n=dPDQPDyDLWVGxIZu&q=85&s=857709c65b37e71489b6b2132879d077" alt="Skills inventory" width="1200" height="900" data-path="images/agent-governance/skills-inventory.webp" />

<YamlTable>
  {`
    - Column: **Skill Name**
    What it shows: The skill name reported by the agent.
    - Column: **Agent**
    What it shows: The agents that loaded this skill.
    - Column: **Endor Score**
    What it shows: A 1-10 trust score (higher is safer), banded as **Unauthorized**, **Caution**, or **Safe** for at-a-glance triage, or **Unscored** if the skill has no score yet. See [Endor Score](/agent-governance/endor-score) for how the score is computed.
    - Column: **Last Active**
    What it shows: When the skill was last used in a session.
    `}
</YamlTable>

Use this to triage low-scoring skills and to coordinate with developers on which skills your organization trusts. Policies can flag or block skills by name or by Endor Score. See [Skill Access](/agent-governance/policies#skill-access) to write those rules.

Select a row to open the skill's detail drawer: usage cards, the frontmatter metadata (description, allowed tools, license, compatibility, file count, and file path), a **Content** section that opens the captured `SKILL.md` in a side panel, and a **Risk** section where **Endor Score Factors** opens the per-dimension score breakdown.

<img src="https://mintcdn.com/endorlabs-b4795f4f/dPDQPDyDLWVGxIZu/images/agent-governance/skill-drawer.webp?fit=max&auto=format&n=dPDQPDyDLWVGxIZu&q=85&s=db85b1e2e2741f7a5a55b99e25cb5ad9" alt="Skill detail drawer showing usage cards, frontmatter metadata, the SKILL.md content entry, and the Risk section" width="2400" height="1800" data-path="images/agent-governance/skill-drawer.webp" />

### How skills are discovered

At every session start, the hook scans for `SKILL.md` files in two directory scopes and reports every skill it finds on the session-start event.

<YamlTable>
  {`
    - Scope: Workspace
    Paths scanned by Claude Code: \`<repository>/.claude/skills/\`
    Paths scanned by Cursor: \`<repository>/.cursor/skills/\`, \`<repository>/.agents/skills/\`, \`<repository>/.claude/skills/\`, \`<repository>/.codex/skills/\` (first workspace root only)
    Paths scanned by Codex: \`.agents/skills/\` in every directory from the repository root down to the working directory
    Paths scanned by GitHub Copilot: \`.github/skills/\` and \`.copilot/skills/\` under the repository root and the working directory
    - Scope: User home
    Paths scanned by Claude Code: \`~/.claude/skills/\`
    Paths scanned by Cursor: \`~/.cursor/skills/\`, \`~/.agents/skills/\`, \`~/.claude/skills/\`, \`~/.codex/skills/\`
    Paths scanned by Codex: \`~/.agents/skills/\`, \`/etc/codex/skills/\`
    Paths scanned by GitHub Copilot: \`~/.copilot/skills/\`
    `}
</YamlTable>

Cursor scans across the common agent skill directories so that a skill installed for any supported agent appears in the inventory.

### `SKILL.md` format

Each skill is a directory that contains a `SKILL.md` file with YAML frontmatter.

```yaml theme={null}
---
name: my-skill
description: One-line summary of what the skill does.
license: Apache-2.0
compatibility: Requires Python 3.14+
allowed-tools:
  - Bash
  - Read
---
# Skill body in Markdown
```

The frontmatter exposes these keys to the Endor Labs inventory:

* `name`: Used as the skill identifier. If it's missing, the inventory uses the parent directory name instead.
* `description`: Shown on the skill row and detail page.
* `license`: Surfaced alongside the skill for compliance review.
* `compatibility`: Free-text dependency requirement.
* `allowed-tools`: List of tools the skill may call. Accepts a YAML list or a space-separated string.

### Discovery limits and behavior

Skill discovery works within the following limits:

* A single event carries up to 100 skills. The hook skips any additional skills.
* The hook truncates each skill's content to 128 KiB and caps a single event payload at 10 MiB. When a skill's content would push the payload past the cap, the hook reports that skill with metadata only.
* The hook skips directories named `.git`, `node_modules`, `vendor`, `__pycache__`, and `.venv` to keep discovery fast.
* The hook doesn't follow symbolic links.
* If two skills share the same `name`, the first one encountered wins. Workspace directories scan before user-home directories.
* The hook skips `SKILL.md` files it cannot read, so a single corrupt skill doesn't break inventory. A readable file with malformed frontmatter still appears, using the directory name as the skill name.

<Note>
  The hook doesn't capture skills the agent loads at runtime from outside the scanned directories.
</Note>

## Next steps

Continue with the following pages:

* See [Write a policy](/agent-governance/policies) to act on what the inventory shows.
* See [Read the Coding Agent Governance overview](/agent-governance/overview) to track inventory trends across a time range.
