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

# How MCP Governance Hooks Work

> How the Lumos hook checks each tool call an AI agent makes, on employee devices and in cloud-hosted agents.

export const HookDiagram = ({cloud = false, dark = false}) => {
  const c = dark ? {
    fg: "#ECEDF1",
    mu: "#9DA1AC",
    su: "#1A1C22",
    ac: "#FF6433",
    ink: "#FF8A63",
    soft: "#2B1911",
    zone: "#1A1512"
  } : {
    fg: "#17181D",
    mu: "#5C606B",
    su: "#FFFFFF",
    ac: "#FE5019",
    ink: "#C23A0C",
    soft: "#FFF1EB",
    zone: "#FFF8F5"
  };
  const id = (cloud ? "hk-cloud-" : "hk-device-") + (dark ? "dark" : "light");
  const mono = "'JetBrains Mono', ui-monospace, Menlo, monospace";
  const Box = ({x, y, w, h, opt}) => <rect x={x} y={y} width={w} height={h} rx="10" fill={c.su} stroke={opt ? c.mu : c.fg} strokeWidth="1.2" strokeDasharray={opt ? "5 4" : undefined} />;
  const Zone = ({y, h}) => <rect x="280" y={y} width="500" height={h} rx="14" fill={c.zone} stroke={c.ac} strokeWidth="1.2" strokeDasharray="6 4" />;
  const LBox = ({y, h}) => <rect x="310" y={y} width="440" height={h} rx="10" fill={c.soft} stroke={c.ac} strokeWidth="1.5" />;
  const T = ({x, y, a = "middle", children}) => <text x={x} y={y} textAnchor={a} fill={c.fg} fontSize="14" fontWeight="650">{children}</text>;
  const S = ({x, y, a = "middle", italic, children}) => <text x={x} y={y} textAnchor={a} fill={c.mu} fontSize="12" fontStyle={italic ? "italic" : undefined}>{children}</text>;
  const M = ({x, y, a = "start", children}) => <text x={x} y={y} textAnchor={a} fill={c.ink} fontFamily={mono} fontSize="11.5">{children}</text>;
  const Z = ({y, name, note}) => <text x="300" y={y} fill={c.ink} fontSize="13" fontWeight="700">{name} <tspan fill={c.mu} fontSize="12" fontWeight="400">· {note}</tspan></text>;
  const E = ({x1, y1, x2, y2, dash, acc}) => <line x1={x1} y1={y1} x2={x2} y2={y2} fill="none" stroke={acc ? c.ac : c.fg} strokeWidth={acc ? 1.8 : 1.4} strokeDasharray={dash ? "4 3" : undefined} markerEnd={`url(#${id}-${acc ? "aa" : "a"})`} />;
  const L = ({x, y, a = "start", tone, children}) => <text x={x} y={y} textAnchor={a} fill={tone === "acc" ? c.ink : tone === "soft" ? c.mu : c.fg} fontSize="12" fontWeight={tone === "soft" ? 500 : 600} paintOrder="stroke" stroke={c.su} strokeWidth="5" strokeLinejoin="round">{children}</text>;
  const N = ({cx, cy, n, acc}) => <>
      <circle cx={cx} cy={cy} r="12" fill={c.su} stroke={acc ? c.ac : c.fg} strokeWidth="1.3" />
      <text x={cx} y={cy + 4} textAnchor="middle" fill={acc ? c.ink : c.fg} fontSize="11.5" fontWeight="700">{n}</text>
    </>;
  const ey = cloud ? 170 : 130;
  const ry = cloud ? 200 : 170;
  return <svg viewBox="0 0 1340 880" width="100%" role="img" style={{
    height: "auto",
    fontFamily: "inherit",
    display: "block"
  }} aria-label={cloud ? "The same request when the agent runs in the cloud. The employee uses a web browser or mobile app, which only shows the chat. The agent harness runs on a cloud machine run by the agent vendor, and the Lumos hook runs inside it. Before a tool runs, the hook sends the call to Lumos, which logs it and allows or denies it. An allowed call goes to an external MCP server, directly or through an MCP gateway, or runs locally on the cloud machine." : "An employee asks an agent harness on their device to pull a customer record from Salesforce and post it in Slack. The model suggests a Salesforce tool call. Before it runs, the Lumos hook in the harness sends the call to Lumos, which logs it and allows or denies it based on policy and the employee's identity. An allowed call goes to an external MCP server, directly or through an MCP gateway, or runs locally on the device."}>
      <defs>
        <marker id={`${id}-a`} viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill={c.fg} /></marker>
        <marker id={`${id}-aa`} viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill={c.ac} /></marker>
      </defs>

      {cloud ? <>
          <Box x="10" y="20" w="200" h="90" />
          <T x="110" y="50">Employee</T>
          <S x="110" y="72" italic>“Pull customer record from</S>
          <S x="110" y="90" italic>Salesforce and post in Slack”</S>
          <E x1="90" y1="110" x2="90" y2="147" />
          <E x1="130" y1="150" x2="130" y2="113" />
          <Box x="10" y="150" w="200" h="100" />
          <T x="110" y="182">Employee's device</T>
          <S x="110" y="204">web browser or mobile app</S>
          <S x="110" y="224">shows the chat, runs no tools</S>
        </> : <>
          <Box x="10" y="100" w="200" h="100" />
          <T x="110" y="136">Employee</T>
          <S x="110" y="160" italic>“Pull customer record from</S>
          <S x="110" y="178" italic>Salesforce and post in Slack”</S>
        </>}

      <Zone y="40" h="520" />
      {cloud ? <Z y="68" name="Cloud machine" note="run by the agent vendor" /> : <Z y="68" name="Employee's device" note="macOS · Windows · Linux" />}

      <Box x="310" y="90" w="440" h="120" />
      <T x="530" y="136">Agent harness</T>
      <S x="530" y="160">{cloud ? "e.g. Claude Code on the web, Codex cloud" : "e.g. Claude Code, Codex, Cursor"}</S>
      <S x="530" y="180">runs the agent loop and calls tools</S>

      <LBox y="290" h="100" />
      <T x="530" y="326">Lumos hook</T>
      <M x="530" y="350" a="middle">PreToolUse · PostToolUse</M>
      <S x="530" y="372">set in the harness's managed config</S>

      <Box x="560" y="440" w="190" h="96" />
      <T x="655" y="466">{cloud ? "Runs on this machine" : "Runs on this device"}</T>
      <S x="655" y="486">Local MCP servers</S>
      <S x="655" y="504">Code execution</S>
      <S x="655" y="522">Browser use, etc.</S>
      <E x1="680" y1="390" x2="680" y2="437" />
      <N cx={655} cy={414} n="6" />

      {!cloud && <>
          <Box x="310" y="440" w="160" h="96" />
          <T x="390" y="470">Device token</T>
          <S x="390" y="492">identifies the employee</S>
          <S x="390" y="512">set once at enrollment</S>
          <E x1="390" y1="440" x2="390" y2="393" />
          <L x="398" y="420" tone="soft">on every call</L>
        </>}

      <Box x="10" y="440" w="200" h="96" />
      <T x="110" y="470">{cloud ? "Admin configuration" : "Your MDM"}</T>
      <S x="110" y="492">{cloud ? "org settings or plugin" : "Jamf · Intune · Kandji"}</S>
      <S x="110" y="512">pushes the hook config</S>
      {cloud ? <>
          <polyline points="210,488 250,488 250,340 307,340" fill="none" stroke={c.fg} strokeWidth="1.4" strokeDasharray="4 3" markerEnd={`url(#${id}-a)`} />
          <L x="242" y="420" a="end" tone="soft">setup, once</L>
        </> : <>
          <E x1="210" y1="488" x2="307" y2="488" dash />
          <L x="258" y="480" a="middle" tone="soft">setup, once</L>
        </>}

      <E x1="210" y1={ey} x2="307" y2={ey} />
      <E x1="310" y1={ry} x2="213" y2={ry} />
      <N cx={230} cy={ey - 18} n="1" />
      <L x="246" y={ey - 14}>Prompt</L>
      <N cx={230} cy={ry + 22 - (cloud ? 0 : 2)} n="9" />
      <L x="246" y={ry + 26 - (cloud ? 0 : 2)}>Response</L>

      <Box x="840" y="100" w="170" h="90" opt />
      <T x="925" y="138">LLM router</T>
      <S x="925" y="160">optional · e.g. LiteLLM</S>
      <Box x="1070" y="100" w="260" h="90" />
      <T x="1200" y="138">Model provider</T>
      <S x="1200" y="160">Anthropic · OpenAI · Bedrock</S>
      <E x1="750" y1="130" x2="837" y2="130" />
      <E x1="840" y1="160" x2="753" y2="160" />
      <E x1="1010" y1="130" x2="1067" y2="130" />
      <E x1="1070" y1="160" x2="1013" y2="160" />
      <N cx={775} cy={112} n="2" />
      <L x="791" y="116">Request</L>
      <N cx={775} cy={198} n="3" />
      <L x="791" y="202">Suggested tool call</L>
      <M x="763" y="222">mcp__salesforce__get_record</M>

      <E x1="490" y1="210" x2="490" y2="287" />
      <L x="480" y="254" a="end" tone="soft">before the tool runs</L>
      <E x1="570" y1="290" x2="570" y2="213" />
      <L x="586" y="254" tone="soft">result</L>

      <Box x="840" y="295" w="170" h="100" opt />
      <T x="925" y="338">MCP gateway</T>
      <S x="925" y="360">optional · e.g. Runlayer</S>
      <Box x="1070" y="295" w="260" h="100" />
      <T x="1200" y="327">External MCP servers</T>
      <S x="1200" y="349">Salesforce · Slack · GitHub</S>
      <S x="1200" y="369">remote, over HTTPS</S>
      <E x1="750" y1="325" x2="837" y2="325" />
      <E x1="840" y1="360" x2="753" y2="360" />
      <E x1="1010" y1="325" x2="1067" y2="325" />
      <E x1="1070" y1="360" x2="1013" y2="360" />
      <N cx={775} cy={307} n="6" />
      <L x="791" y="311">Allowed call</L>
      <N cx={775} cy={386} n="7" />
      <L x="791" y="390">Result</L>

      <E x1="500" y1="390" x2="500" y2="727" dash acc />
      <E x1="540" y1="730" x2="540" y2="393" dash acc />
      <N cx={465} cy={575} n="4" acc />
      <L x="449" y="579" a="end" tone="acc">Check the call</L>
      <L x="449" y="597" a="end" tone="soft">PreToolUse</L>
      <N cx={465} cy={640} n="8" acc />
      <L x="449" y="644" a="end" tone="acc">Log the outcome</L>
      <L x="449" y="662" a="end" tone="soft">PostToolUse</L>
      <N cx={575} cy={575} n="5" acc />
      <L x="591" y="579" tone="acc">Allow / deny</L>

      <Zone y="690" h="170" />
      <Z y="718" name="Lumos" note="Lumos-hosted" />
      <LBox y="730" h="110" />
      <T x="334" y="764" a="start">Lumos MCP Server</T>
      <S x="334" y="790" a="start">1. Logs every tool call</S>
      <S x="334" y="812" a="start">2. Allows or denies it based on policy and agent identity</S>
    </svg>;
};

Lumos sees every tool call an AI agent makes and decides whether to allow or deny it. That includes calling an MCP server (for example, sending a Slack message) and running a local tool (for example, a Bash command or browser use).

Lumos uses the ToolUse hook interface, which every major agent harness supports, including Claude Code, Codex, and Cursor. It works with any model and on every operating system (macOS, Windows, Linux). It also works for cloud agents. When you use Claude Code on the web, the harness runs on an Anthropic server and still calls the Lumos hook. Lumos doesn't support all of these clients yet; see [client coverage](#client-coverage) below.

<Tabs>
  <Tab title="Harness on employee device">
    <div className="block dark:hidden">
      <HookDiagram />
    </div>

    <div className="hidden dark:block">
      <HookDiagram dark />
    </div>
  </Tab>

  <Tab title="Cloud-hosted agent">
    <div className="block dark:hidden">
      <HookDiagram cloud />
    </div>

    <div className="hidden dark:block">
      <HookDiagram cloud dark />
    </div>
  </Tab>
</Tabs>

## The path of a tool call

1. **Prompt.** The employee asks the agent to pull a customer record from Salesforce and post it in Slack.
2. **Request.** The agent sends the prompt to the model, directly or through your LLM router.
3. **Suggested tool call.** The model replies with a tool to run, here Salesforce's `get_record`.
4. **Check the call.** Before the tool runs, the hook's `PreToolUse` event sends the tool name and arguments to Lumos.
5. **Allow or deny.** Lumos checks your policies against the call and the person's identity, such as groups, department, and title. A denied call stops here, and Lumos logs it as denied.
6. **Allowed call.** The tool runs on an external MCP server, directly or through your MCP gateway, or locally, like Bash or a local MCP server.
7. **Result.** The output returns to the agent.
8. **Log the outcome.** After the tool runs, the hook's `PostToolUse` event reports the completed call to Lumos, which logs it.
9. **Response.** The employee sees the answer.

Steps 2 through 8 repeat for every tool call: here once for Salesforce, then again for Slack.

## Rollout

* **Your MDM pushes the hook config** (Jamf, Intune, or Kandji) as managed settings employees can't edit or remove. No Lumos software is installed.
* **Each device enrolls once** and gets a device token. Lumos attributes every call to the person your device inventory assigns that device to.
* **For a trial**, one command installs the hooks in a user's own settings, without an MDM.

## LLM routers and MCP gateways

Both stay in place. Lumos adds per-person policy and a log of every call.

* **LLM routers** change where inference goes, not where tools run, so the hook works the same.
* **MCP gateways** see a call only after Lumos allows it. Built-in tools and local MCP servers never pass through a gateway, and the hook still covers them.

## Verdicts and timing

* **Allow is silent.** The agent's own permission prompts still apply.
* **Deny stops the call** and names the policy: *Blocked by your organization's Lumos policy "\<name>"*.
* **Checks time out after 4 seconds.** The call proceeds and is logged as not evaluated.
* **Unenrolled or revoked devices are denied.**
* **Policy edits apply within seconds.** Identity changes, such as a group removal, apply within 5 minutes.

## What Lumos receives

| Field | Detail |
| - | - |
| **Who** | The person the device is enrolled to. |
| **What** | The tool name, MCP server, arguments, and session. Not the conversation. |
| **Stored** | A summary of each call. Values that look like secrets are redacted. |

## Client coverage

For which harnesses, operating systems, and cloud agents Lumos supports today, see [Supported agents and clients](/mcp-governance#supported-agents-and-clients).
