How do you detect shadow AI?
To detect shadow AI, combine sources that each see a different layer. Identity grants and expense data show sign-ups, DNS and gateway logs show which services the network reached, and MDM inventory and config files show what's installed. Endpoint network observation ties each connection to a signed app and a person, and coding agent hooks show what an approved agent did. Start with the sources that need no deployment. No network method can tell a personal ChatGPT or Claude account from a company one on the same domain; that takes the vendor's tenant controls.
Shadow AI here means AI tools and accounts that people use but that you haven't approved or can't see: a chatgpt.com tab, a Cursor install pointed at an MCP server from a blog post, a model in Ollama, or a developer on a staff laptop who runs claude with a personal Max plan. The following sections take each method in turn, with the signal, a command or query, what it misses, and the effort.
Which detection methods find which AI tools?
The following table summarizes the eight methods. Effort assumes that the underlying system, such as a resolver that keeps query logs, is already in place.
| Method | Signal | Misses | Effort |
|---|---|---|---|
| DNS and resolver logs | Hostnames each client looked up | DNS over HTTPS, off-network devices, account type | Hours |
| Gateway, SSE, and CASB logs | Hostname, user, and app category; URLs with TLS inspection | QUIC, bypassed apps, traffic that skips the gateway | Hours |
| Identity provider grants | OAuth apps that people authorized with a work identity | Sign-ups with a personal email, API keys | An hour |
| Expense and card data | Merchant, amount, and employee | Free tiers, personal cards that are never expensed | An hour |
| MDM software inventory | Installed app bundles and versions | CLIs, IDE extensions, browser use | An hour |
| Config file inventory | Agent and MCP config files in each home directory | Cloud agents, relocated config, which account is used | A day |
| Endpoint network observation | Connection, signed process, and local user | Prompt content, account type, local models | Days |
| Coding agent hooks | Each tool call, prompt, and model switch | Clients without the hook installed | Days |
How do DNS and resolver logs show AI use?
The signal is the hostname that each client asks your resolver to look up. AI services use recognizable names: api.anthropic.com and claude.ai for Anthropic, chatgpt.com and api.openai.com for OpenAI, api2.cursor.sh for Cursor, and plan-specific hosts under githubcopilot.com for GitHub Copilot. Copilot is the one case where a hostname reveals the account type, because GitHub routes each plan to its own host, and a lookup under individual.githubcopilot.com means an individual plan. For the plan hostnames, see blocking personal AI accounts on work laptops.
If your resolver writes one line per query with client= and query= fields, the following command counts AI lookups per client. Adjust the two field prefixes to match your log format:
awk '{ for (i = 1; i <= NF; i++) { if ($i ~ /^client=/) c = substr($i, 8) if ($i ~ /^query=/) q = tolower(substr($i, 7)) } if (q ~ /(^|\.)(anthropic\.com|claude\.ai|openai\.com|chatgpt\.com|cursor\.sh|githubcopilot\.com|perplexity\.ai|gemini\.google\.com|deepseek\.com|x\.ai|grok\.com|mistral\.ai)$/) print c, q}' resolver.log | sort | uniq -c | sort -rnEach output line is a count, a client address, and a hostname, such as 14 10.0.4.21 api.anthropic.com. The pattern matches whole labels, so notanthropic.com doesn't match. On one Windows machine, Get-DnsClientCache | Where-Object Entry -match 'anthropic|openai|chatgpt|cursor' lists matching names in the local cache, which holds only unexpired entries.
DNS misses any browser or app that uses DNS over HTTPS to a public resolver, and any laptop on a home network or hotspot without an always-on VPN. The log names a client address, so you need DHCP or VPN records to find the person, and a lookup isn't use, because browsers prefetch names. Effort is a few hours if your resolver keeps query logs.
What do secure web gateway, SSE, and CASB logs show?
A secure web gateway, security service edge (SSE) platform, or cloud access security broker (CASB) records the hostname, the authenticated user, and an app category for each request. With TLS inspection on, it also records the URL and can inspect the request body. Most products ship a generative AI category, so the first query is that category grouped by user.
In Microsoft Defender for Cloud Apps, for example, go to Cloud apps > Cloud discovery, open the Discovered apps tab, filter Category to Generative AI, and sort by users (Microsoft Learn). Cloud discovery builds the list from firewall and proxy logs, or from Defender for Endpoint.
A gateway misses QUIC traffic, apps on its inspection bypass list, split-tunnel traffic, and laptops off the gateway, and it can't separate personal from company sessions on chatgpt.com unless it inserts the vendor's tenant header. The section on where network detection goes blind covers each gap. Effort is a few hours if a gateway is already deployed.
What do identity provider and OAuth grants show?
When someone signs in to an AI tool with their work Google or Microsoft account, the identity provider records an OAuth grant: the app, the user, and the scopes. A grant with only openid, email, and profile means a sign-up. A grant with Drive, Gmail, or Graph file scopes means that the tool can read company data, so review those first.
In the Google Admin console, go to Menu > Security > Access and data control > API controls > Manage App Access, and in Accessed apps, click View list. Download list exports a CSV with the API scopes for each app, and the list updates 48 hours after a grant (Google Workspace Admin Help). For per-user events, go to Reporting > Audit and investigation > OAuth log events (Google Workspace Admin Help).
In Microsoft Entra, the portal path is Entra ID > Enterprise apps > All applications, then an app's Permissions page, which has Admin consent and User consent tabs (Microsoft Learn). To see every user-consented app across the tenant, run the following Microsoft Graph PowerShell, which needs only Directory.Read.All:
Connect-MgGraph -Scopes "Directory.Read.All"Get-MgOauth2PermissionGrant -All | Where-Object ConsentType -eq "Principal" | Group-Object ClientId | ForEach-Object { $sp = Get-MgServicePrincipal -ServicePrincipalId $_.Name [pscustomobject]@{ App = $sp.DisplayName Publisher = $sp.PublisherName Users = ($_.Group.PrincipalId | Sort-Object -Unique).Count Scopes = ($_.Group.Scope | Sort-Object -Unique) -join " " } } | Sort-Object Users -DescendingA Principal grant is one that a user made for themselves (Microsoft Graph reference), and the output is one row per app with its distinct users and scopes.
Grants miss sign-ups with a personal email or a password, and API keys. A grant doesn't show the plan either: a ChatGPT account created with a work Google login sits outside your workspace unless someone added it. Effort is about an hour.
What do expense and corporate card data show?
Paid AI plans leave a merchant name on a card statement or an expense claim. Export 12 months of transactions and search the merchant and description fields:
grep -iE 'openai|chatgpt|anthropic|claude|cursor|anysphere|perplexity|copilot' expenses.csvAnysphere, Inc. makes Cursor (Cursor terms of service). Matching rows show who paid, how much, and since when. Expense data misses free tiers and unclaimed personal cards, but each row names someone whose work breaks first when you block personal accounts. Effort is about an hour.
What does MDM software inventory show?
Your MDM already collects installed applications. In Microsoft Intune, go to Apps > Monitor > Discovered apps and select Export. Company-owned Macs report all installed apps, and Windows reports Windows Installer apps from Add or remove programs every 24 hours (Microsoft Learn). In Jamf Pro, create an advanced computer search with the Application Title criterion. On one Mac, search by bundle ID, which finds an app wherever it's installed, including ~/Downloads:
for id in com.todesktop.230313mzl4w4u92 com.anthropic.claudefordesktop \ com.openai.chat com.openai.codex; do mdfind "kMDItemCFBundleIdentifier == '$id'"doneThe four IDs are Cursor, Claude, ChatGPT, and Codex, and each output line is the path to a matching .app bundle.
App inventory misses command-line tools installed with a native installer, npm, or Homebrew, IDE extensions such as GitHub Copilot or Cline, and browser use, and it says nothing about use or account. Effort is about an hour.
What do config files on developer machines show?
Coding agents write configuration to fixed paths in each home directory, so a file is a reliable install signal for tools that app inventory misses, and it lists the MCP servers that each agent connects to.
| Tool | Path | What it holds |
|---|---|---|
| Claude Code |
| Settings and session history in |
| Cursor |
| Global and project MCP servers |
| OpenAI Codex |
| Settings and one |
| Claude Desktop | on macOS, | Local MCP servers |
Sources: Claude Code settings, Claude Code MCP, Cursor MCP, Codex MCP, and Claude Desktop. On one machine, ls -la ~/.claude ~/.cursor ~/.codex shows which tools ran at least once, and the modification times give a rough last use. To list MCP server names without reading anything else, run the following commands:
jq -r '[.mcpServers // {}, (.projects // {} | .[] | .mcpServers // {})] | add | keys[]' ~/.claude.jsonjq -r '.mcpServers // {} | keys[]' ~/.cursor/mcp.jsonsed -n 's/^\[mcp_servers\.\([^].]*\)\]$/\1/p' ~/.codex/config.tomlEach command prints one server name per line, such as github or stripe. For what to do with the list, see restricting MCP servers in coding agents. Across a fleet, push an MDM script that returns only which paths exist and the server names; ~/.claude.json holds account details, so don't collect it wholesale.
Config files miss tools you didn't list, config moved by a variable such as CLAUDE_CONFIG_DIR, cloud agents, and the account in use. Effort is about a day. For which file names the signed-in account, and what each source of evidence proves for a coding agent, see shadow AI coding agents.
What does endpoint network observation show?
On the device, you can see a connection, the process that opened it, its code signature, and the local user together, which ties a command-line agent on a laptop in a café to a person. On a Mac, the following command lists open connections from likely AI processes, including other users' processes:
sudo lsof -i -nP | grep -iE 'claude|cursor|codex|chatgpt|node'Each line shows the process name, PID, user, and a NAME column such as 192.0.2.2:59988->160.79.104.10:443 (ESTABLISHED). lsof truncates names to nine characters, so a Cursor helper appears as Cursor\x20H, and a Claude Code install from npm runs as node. The remote side is an IP address. Anthropic publishes its inbound ranges, including 160.79.104.0/23 (Anthropic IP addresses), and when reviewed on November 3, 2026, api.anthropic.com and claude.ai both resolved to 160.79.104.10, while chatgpt.com resolved to shared Cloudflare addresses that also serve unrelated sites.
A process name is easy to change, so check the code signature. First verify that the app still matches its signature, then read who signed it:
codesign --verify --strict --verbose=2 /Applications/Cursor.appcodesign -dv --verbose=2 /Applications/Cursor.app 2>&1 | grep -E '^(Identifier|TeamIdentifier|Authority)='The first command exits with status 0 and reports valid on disk and satisfies its Designated Requirement only when the bundle is intact. If it fails, the second command's output isn't evidence of anything. For Cursor, the second command's output includes Identifier=com.todesktop.230313mzl4w4u92 and TeamIdentifier=VDXQ22DGB9. Apple assigns the Team ID to the developer account that signed the code, so a renamed or repackaged binary can't present it without that developer's signing key. The Team IDs that Arcjet's detection rules match for other common clients are Q6L2SF6YDW for Anthropic (Claude and the native Claude Code binary) and 2DC432GLL2 for OpenAI (ChatGPT and Codex).
On Windows, join Get-NetTCPConnection to Get-Process in an elevated PowerShell session:
Get-NetTCPConnection -State Established -RemotePort 443 | ForEach-Object { $p = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue [pscustomobject]@{ Process = $p.ProcessName Path = $p.Path Remote = $_.RemoteAddress } } | Where-Object Process -match 'claude|cursor|codex|chatgpt|node'Each row is a process name, its executable path, and a remote address. To check the signer, run Get-AuthenticodeSignature -FilePath PATH | Select-Object Status, @{n='Signer'; e={$_.SignerCertificate.Subject}}, replacing PATH with a value from the Path column. Trust the signer only when Status is Valid.
Both commands are snapshots that miss short-lived connections. For continuous coverage, you need software that records every connection. If you run Microsoft Defender for Endpoint, the DeviceNetworkEvents table already holds the remote URL, process, and account for each connection (Microsoft Learn):
DeviceNetworkEvents| where Timestamp > ago(7d)| where RemoteUrl has_any ("anthropic.com", "claude.ai", "openai.com", "chatgpt.com", "cursor.sh", "githubcopilot.com")| summarize Connections = count(), LastSeen = max(Timestamp) by DeviceName, InitiatingProcessFileName, InitiatingProcessAccountUpn, RemoteUrl| order by Connections descEndpoint observation misses prompt content, account type, local models, and devices without the software. A connection from an AI app also isn't proof of an AI request, because it can be an update check. Effort is minutes per machine by hand and days to deploy across a fleet.
What do coding agent hooks show?
Claude Code, GitHub Copilot, Cursor, OpenAI Codex, and Muse Code fire hooks before a tool runs and when a prompt is submitted. A hook installed through managed settings or MDM records what an agent did: a connection log shows that claude reached api.anthropic.com, and a hook event shows that the session ran curl -X POST to an unknown host. To check that managed settings reach a machine, run /status in Claude Code and read the Setting sources line.
A hook covers only the client where it's installed, so a second agent, a cloud session, or an unmanaged machine never fires it, and some hooks fail open. To find sessions that never reached a hook, add OpenTelemetry or the Claude Compliance API and look for sessions with no hook decisions (Arcjet observe). Effort is a few days, mostly reviewing a starter policy in dry run.
Where does network detection go blind?
DNS logs, gateways, and endpoint observers that read the TLS server name all depend on a visible hostname. The following conditions remove it or make it ambiguous.
QUIC and HTTP/3
HTTP/3 runs over QUIC on UDP port 443, which TCP-based proxies and inspection engines can't read. Zscaler advises customers "to block QUIC and fallback to TCP" (Zscaler, October 8, 2024). AI services advertise HTTP/3: when reviewed on November 3, 2026, the HTTPS DNS records for claude.ai, perplexity.ai, and grok.com listed alpn=h3,h2. To check a domain, query its HTTPS record:
curl -s -H 'accept: application/dns-json' \ 'https://cloudflare-dns.com/dns-query?name=claude.ai&type=HTTPS'Block outbound UDP 443 on the network or device firewall so that browsers fall back to TCP.
Encrypted Client Hello
Encrypted Client Hello (ECH) encrypts the server name that SNI-based tools read. The IETF published it as RFC 9849 in March 2026. Chrome has enabled ECH by default since version 117 (Chrome Platform Status), and Firefox made it available in version 119. Cloudflare enables ECH by default on Free zones, and every Cloudflare site that uses it shares the outer name cloudflare-ech.com (Cloudflare docs), which is all that an SNI-based tool sees.
When reviewed on November 3, 2026, none of the HTTPS records for chatgpt.com, claude.ai, perplexity.ai, cursor.com, grok.com, or chat.deepseek.com published an ECH configuration, so the exposure is smaller AI tools on Free plans. Cloudflare documents resolver-side controls for Chrome and Firefox, and the EncryptedClientHelloEnabled policy turns ECH off in managed Chromium-based browsers (Microsoft Edge policy reference).
Certificate pinning and inspection bypass lists
TLS inspection presents a certificate from your own root, and apps that pin their certificates refuse it. dope.security, which sells an on-device secure web gateway, writes that such apps "refuse to trust a MITM'd one", so its agent "has to maintain a bypass list for those" (dope.security, January 29, 2026). Vendors also ask for exclusions: Cursor's enterprise network guide recommends disabling SSL inspection for .cursor.sh, cursor.com, and related domains (Cursor docs). On every bypassed destination, the gateway sees a hostname at most, and no content.
Personal hotspots and off-VPN laptops
A laptop on a phone hotspot or home network without an always-on VPN skips your resolver and network gateway, so their logs show nothing. Software on the device keeps working. A personal phone or laptop is outside every method here except expense data.
Local models
A local model makes no outbound connection when it answers a prompt. Ollama binds to 127.0.0.1 on port 11434 by default, and its FAQ says "we don't see your prompts or data when you run locally" (Ollama FAQ). Only the model download crosses the network. Find local models with inventory instead: the Ollama app in MDM, or a listener found with lsof -iTCP:11434 -sTCP:LISTEN.
One domain for personal and company accounts
Personal and Enterprise sessions of ChatGPT both go to chatgpt.com, and personal and Enterprise sessions of Claude both go to claude.ai. DNS, SNI, and IP addresses can't separate them. The vendors provide tenant controls that do: the anthropic-allowed-org-ids header for Claude, the ChatGPT-Allowed-Workspace-Id header for ChatGPT, plan-based hostnames for Copilot, and the AllowedTeamId MDM policy for Cursor. For each control, see how to block personal AI accounts on work laptops.
How do you run a one-week shadow AI discovery?
The following procedure starts with sources that need no deployment and adds device-level methods once you know where to look. Each step adds rows to the inventory described in the next section.
- On day 1, export Google Workspace accessed apps or run the Entra grant script, and export 12 months of card and expense data. Flag grants with file or mail scopes first.
- On day 2, search 30 days of resolver logs for AI hostnames and run your gateway's generative AI report grouped by user. Map client addresses to people.
- Also on day 2, confirm that outbound UDP 443 is blocked, list the domains on your TLS inspection bypass list, and note which devices work off the VPN.
- On day 3, export MDM inventory and search for Cursor, Claude, ChatGPT, Codex, Ollama, and other AI clients.
- On days 3 and 4, push the read-only config file script to developer machines and record each agent and its MCP servers.
- On day 4, deploy endpoint network observation to a pilot group that includes the heaviest users found so far, or run the
DeviceNetworkEventsquery. Record the signed app, provider, and person for each connection. - On day 5, merge the findings into one record per tool, account, person, and device, and assign an owner and a decision to each. Install hooks for approved coding agents and publish a starter policy in dry run. Apply the vendor tenant control for each tool that moves to a company account.
After the first week, repeat steps 1 and 2 monthly, and let continuous sources, such as endpoint observation and hooks, add records as they appear.
What do you record for each finding?
Samsung's semiconductor division permitted ChatGPT from March 11, 2023, and within 20 days recorded three incidents: two engineers pasted source code into it, and one pasted a meeting transcript to generate minutes (The Economist Korea, March 30, 2023). The tool was approved and the data wasn't, so a record needs a data class as well as a tool name.
Keep one record per tool, account, person, and device, with the following fields:
| Field | What to record | Example |
|---|---|---|
| Tool | Product and surface: browser, desktop app, CLI, IDE extension, or MCP server | Claude Code (CLI) |
| Account type | Company tenant, personal, or unknown | Personal (Max plan, expensed) |
| Person | The person behind the account, or unknown for shared accounts | dana@example.com |
| Device | Managed device name or serial, or unmanaged | MBP-0412 |
| Data class | The most sensitive data the tool can reach | Source code, production read access through an MCP server |
| Evidence | Each source and what it showed | Endpoint connection, config file, expense line |
| Owner | The team accountable for the decision | Platform engineering |
| Decision | Approve, approve with conditions, move to company tenant, or block | Move to company tenant and install hooks |
| Review date | When to look at the record again | 2027-02-01 |
Record "unknown" when a source can't establish a field, such as the account type behind a connection. Keep evidence per source, because each proves something different: a connection proves that an app reached a provider, a hook event proves what an agent did, and a Compliance API record proves that a company account had a session.
Where does Arcjet fit in shadow AI detection?
Arcjet covers the endpoint and coding agent layers of this guide. The Arcjet endpoint agent for macOS and Windows observes connections from each managed device to AI services, identifies the signed app that made each one, and attributes it to the person behind the local account at the time. On macOS, it's a Network Extension content filter with a root launchd service, so closing the menu bar app or logging out doesn't stop managed reporting. On Windows, it's a LocalSystem service with no kernel driver. Neither platform decrypts TLS, installs a root certificate, or reads prompts, so pinned apps and command-line agents keep working.
You deploy the agent through your MDM, including Jamf, and an administrator maps local accounts to people. Shared and unidentified accounts show as Unknown user and don't inherit the device owner. In the Arcjet Console, Discovery leads from People to Devices to AI usage, and a Providers view shows which people, devices, and apps reach a service. Counts are connections, not prompts. Endpoint provider policies set Allow or Block per provider for a group of devices, and each block lands in Arcjet Activity with the person, device, app, and provider. For coding agents, Arcjet enforces one policy in the hooks that Claude Code, GitHub Copilot, Cursor, OpenAI Codex, and Muse Code already fire.
Arcjet has the limits described on this page. An ECH connection shows only the outer name. A hostname can't tell a personal account from a company one, so pair Arcjet with the vendor tenant controls. Local models make no connection, and devices without the agent, including personal phones, are out of scope. Arcjet doesn't inspect what someone types into a browser tab; if that's your main concern, a browser extension such as Harmonic, which inspects prompts and identifies the account behind each session, fits better. If most of your devices aren't managed, start with identity grants and expense data.
To compare products that automate these methods, see shadow AI detection tools. For pricing and availability of the endpoint agent, book a demo. To start with coding agents, follow the Arcjet coding agents guide and publish a starter policy in dry run.
Frequently asked questions
What is the fastest way to find shadow AI?
Start with sources you already have. Export OAuth grants from Google Workspace or Microsoft Entra, search 12 months of expense and card data for AI vendors, and query resolver or gateway logs for AI hostnames. These take hours, need no deployment, and tell you which people and devices to look at with endpoint methods.
Can DNS logs tell personal ChatGPT or Claude use from company use?
No. Personal and company sessions go to the same domains, such as chatgpt.com and claude.ai. GitHub Copilot is the exception, because it routes individual plans to hosts under individual.githubcopilot.com. For Claude, ChatGPT, and Cursor, use the vendor tenant controls: anthropic-allowed-org-ids, ChatGPT-Allowed-Workspace-Id, and AllowedTeamId.
How do you find AI coding agents on developer laptops?
App inventory misses most of them, because CLIs install through npm, Homebrew, or a native installer. Check for config files instead: ~/.claude/ and ~/.claude.json for Claude Code, ~/.cursor/mcp.json for Cursor, and ~/.codex/config.toml for Codex. The same files list the MCP servers each agent connects to.
Does QUIC or Encrypted Client Hello hide AI traffic?
They can. QUIC runs over UDP 443, which TCP-based inspection can't read, so block UDP 443 and browsers fall back to TCP. Encrypted Client Hello hides the server name behind an outer name such as cloudflare-ech.com. Chrome and Firefox support it, and Chromium-based browsers have an EncryptedClientHelloEnabled policy to turn it off.
Can you detect a local model such as Ollama?
Not on the network. A local model makes no outbound connection when it answers a prompt. Find it with software inventory or by checking for a listener on Ollama's default port, 11434.
Does the Arcjet endpoint agent decrypt TLS?
No. It reads the server name from the TLS handshake, identifies the signed app that opened the connection, and attributes it to a person. It doesn't install a root certificate or read prompts, so it can't see prompt content in a browser tab or tell a personal account from a company one.
AI runtime security in your code
Protect your AI agent workflows with Arcjet
Install Arcjet hooks on your approved coding agents and publish a starter policy in dry run to see what each session does.