On this page
Scanning

AI agent posture

What Aevrin discovers about the coding agents on a machine, the MCP trust grade, and what never leaves your device.

Aevrin's MCP scanner answers whether a server is safe to install. Agent posture answers the question around it: which AI coding agents are on this machine, what each has been allowed to do, and what that conclusion rests on.

Configuration is read and nothing else. No agent is started, no command from a configuration file is executed, and nothing is uploaded.

What is supported today

Claude Code and Codex. Cursor and Gemini CLI are not yet discovered; when they are, they will appear through the same normalised model rather than a parallel one.

Everything below comes from each vendor's own published configuration reference.

Claude Code

WhatWhere it is read from
Managed policy/Library/Application Support/ClaudeCode/managed-settings.json (macOS), /etc/claude-code/managed-settings.json (Linux, WSL), C:\Program Files\ClaudeCode\managed-settings.json (Windows)
User settings~/.claude/settings.json
Project settings<project>/.claude/settings.json
Local settings<project>/.claude/settings.local.json
MCP servers, user scope~/.claude.json
MCP servers, project scope<project>/.mcp.json
Skills~/.claude/skills/<name>/SKILL.md and <project>/.claude/skills/<name>/SKILL.md
Plugin marketplaces~/.claude/plugins/

Codex

WhatWhere it is read from
Settings, MCP servers, project trust$CODEX_HOME/config.toml, defaulting to ~/.codex/config.toml
Sign-in state$CODEX_HOME/auth.json (presence only)

Codex differs from Claude Code in a way that is preserved rather than smoothed over: there is no per-project settings file. approval_policy and sandbox_mode are global and apply in every directory, and the only project-scoped setting is trust_level. Codex also has no project-level MCP file, so every Codex MCP server is global.

What Codex can reach is decided by its sandbox, not by a permission list:

sandbox_modeFilesystem readFilesystem writeShellNetwork
read-onlyFull: the whole filesystem, not just the workspaceNoneLimitedNone
workspace-writeFullLimited to the workspaceLimitedNone unless sandbox_workspace_write.network_access = true
danger-full-accessFullFullFullFull

approval_policy = never does not widen the sandbox. It removes the human in front of what the sandbox already allows, which is the difference between a supervised shell and an unattended one, and it is recorded as evidence against every capability it affects.

An unset sandbox_mode is reported as unknown, not as none. A config that does not say is not a config that grants nothing, and coverage names the gap. Codex profiles can override the sandbox, and which profile is active is a runtime argument rather than a file, so a config with profiles is reported as partial coverage too.

A server with enabled = false is listed and marked disabled rather than dropped: it is still configured, and enabling it is one edit away.


Each discovered item keeps the file it came from and its scope, because the same permission means different things in different places: one an organisation pushed through managed policy is a deliberate decision, and the same rule in a local file is one developer's shortcut.

Running it

aevrin agent scan

Add --project to include a specific project's configuration, and --verbose to list every file that was read:

aevrin agent scan --project ./my-repo --verbose

Every configured agent is reported, each with its version, counts and coverage, then its capabilities with the evidence behind each one:

Claude Code
Version: 2.1.220 (Claude Code)
MCP: 8   Capabilities: 5   Permissions: 18   Skills: 1
Coverage: complete

Capability       Level    Evidence
Filesystem read  LIMITED  permissions.additionalDirectories: ../shared-ui
Network          LIMITED  permissions.allow: WebFetch(domain:raw.githubusercontent.com)
Shell            LIMITED  permissions.allow: Bash(npm run *)  (+11 more)

MCP servers configured
  github  user scope, stdio
      npx -y @modelcontextprotocol/server-github

How permissions are read

Effective capability is the widest grant across every file, not the winner of precedence. Precedence decides which setting applies; it does not narrow what the agent can reach. An agent with Bash(npm run *) in project settings and an unrestricted Bash in user settings can run anything, and is reported that way.

A few consequences worth knowing:

  • Bash(npm run *) is reported as limited, not full shell. It is the most common safe setup there is, and calling it unrestricted would make the report useless.
  • A deny rule is recorded as evidence but never removes a capability. It narrows one path while the agent keeps that capability everywhere else.
  • A hook grants shell whatever the permissions block says, because it runs a command on the agent's behalf with the agent's privileges.
  • permissions.defaultMode: bypassPermissions grants every capability, and is called out.
  • An MCP rule is read like any other rule. mcp__github and mcp__github__* allow every tool on the github server (full), mcp__github__create_issue allows one tool (limited), and the same rule under ask is ask. A deny rule is kept as evidence and grants nothing, so a server that only appears in deny rules is not listed as something the agent can use.
  • enableAllProjectMcpServers marks project servers auto-approved, since it removes the one human checkpoint in front of an untrusted server.

On the dashboard

After aevrin agent scan --upload, each agent's page answers, in everyday words: how safe is this agent, why it got its score, what to do, what it can reach, and which rules give it that access. Each rule is shown exactly as written, next to what it does. The exact data (capability ids, levels, the technical reasons, file paths) is one click away under Technical details.

On the pageMeans
Safety scoreThe agent posture score, 0 to 100. Higher is safer. It starts at 100 and loses points for each reason listed.
What it can reachThe widest access in any settings file, not only the setting that wins.
Everything, Some, Only if you say yes, No, Could not tellLevels full, limited, ask, none and unknown. Could not tell is counted as the most it could allow.
Gives access, Asks you first, Limits or blocksA rule's effect: allow, ask, deny.

Why is this agent high risk? asks your configured AI provider to explain the score from the same data the page shows. It cannot change the score.

JSON output

aevrin agent scan --json

Returns a versioned snapshot. schema_version is "1"; it is bumped when the shape changes in a way a consumer must notice.

{
  "agents": [
    {
      "schema_version": "1",
      "agent": { "type": "claude_code", "name": "Claude Code", "version": "...", "install_path": "..." },
      "device": { "hostname": "...", "platform": "Windows", "platform_version": "10" },
      "permissions": [{ "rule": "Bash(npm run *)", "effect": "allow", "scope": "user", "source_path": "..." }],
      "capabilities": [{ "capability": "shell", "level": "limited", "evidence": [] }],
      "mcp_servers": [], "skills": [], "plugins": [], "hooks": [],
      "credentials": [{ "kind": "github_token", "present": true, "source": "environment", "location": "GITHUB_TOKEN" }],
      "coverage": { "checked": [], "not_checked": [], "complete": true }
    }
  ]
}

The same machine and configuration produce the same JSON, so two snapshots can be diffed.

Sending it to your dashboard

aevrin agent scan --upload

The snapshot is stored against your account, one record per machine and agent. Running it again from the same machine replaces that machine's record rather than adding to a list: the snapshot describes what is true there now.

What is sent is the same JSON --json prints, with one change: anything in a configured MCP server's arguments or address that looks like a secret (--token X, --api-key=X, a password in a URL, a ?token= value) is replaced with [REDACTED] before it leaves your machine. No source code, no file contents, and no credential value. The upload counts as one agent posture scan (see Usage and plan limits), not as a CLI or dashboard scan: no scanner runs on the server, it stores the document your machine already produced.

Once a device has reported, the dashboard shows it under AI security:

PageWhat is on it
AgentsEvery agent that has reported, its device, its posture score and every deduction behind it
An agentCapabilities with their evidence, MCP servers, skills, plugins, hooks, permission rules, credentials in reach, and every file that was read
DevicesEvery machine that has reported, rated by the least restricted agent on it
MCP serversEvery MCP server across every device, one row per server, with its scopes and trust grade
SkillsEvery skill installed, one row per installation, with its scope and source file
PermissionsEvery rule across every device, exactly as written, with the file it lives in
Attack pathsWhat an agent can reach, when there is evidence for every step

A device is rated by its worst agent, not an average: averaging lets one tidy agent hide the one that can reach everything.

Skills are listed one row per installation rather than correlated the way MCP servers are. A skill is a folder of prose on a machine with no URL or package to pin it to, so two skills sharing a name are not evidence of being the same skill.

The dashboard is read-only about your machines. It cannot add, remove, disable or edit an MCP server, and it cannot change a permission. Those live in files on your machine and are changed there. Aevrin has no channel to your laptop, and a page offering a button that silently does nothing would be worse than one that says so.

Forget a device removes Aevrin's copy of what it reported. Nothing on the machine changes, and the next --upload from it reports it again.

Scanning the servers your agents use

aevrin agent scan --scan-servers --upload

--scan-servers scans each MCP server your agents are configured with, on this machine, the same way aevrin scan mcp does, after the posture report. It scans each package or address once: an npm package as npx -y <package>, a PyPI package as uvx <package>, a public HTTPS address as itself. It never uses the arguments or environment you configured, because they often hold secrets. It shows every command and asks before it starts (--yes skips the question), scans at most 10, and needs Docker. With --upload, each scan is saved, and the MCP servers page then shows its grade. See agent scan for the exit codes.

Agent posture score

Four numbers exist in Aevrin and they answer different questions. Keeping them apart is the point:

NumberQuestion it answers
MCP scan scoreHow many problems does this server have?
MCP trust grade (A-F, or no grade when the scan could not establish one)Should I let this server run?
Agent posture scoreHow much can this agent already do on this machine?
Blast radiusWhat does that reach if it is misused? Surfaced as posture factors, not a fifth number.

Posture starts at 100 and every deduction arrives with the sentence that earned it:

Claude Code
Posture: 80/100  MEDIUM   confidence: high
  -5   shell access, limited to specific commands
  -15  credentials reachable from a shell this agent can use: aws_credentials_file, claude_code_credentials

The rubric, in one place so it can be argued with:

DeductionWhy
Unrestricted shell −15, command-scoped shell −5Bash(npm run *) is the most common safe setup there is
Unrestricted file writes −10, workspace writes −4
Unrestricted network −10, reads any file −3
Nothing put to a human first −15Adds no capability; removes the check on every capability already there
Credentials reachable from a shell −15The blast radius. The credential is not the risk, the combination is
MCP servers auto-approved −5 each, capped at −15Their one human checkpoint is gone
A server graded D −20, graded C −8Only ever from a grade a scan actually produced
Hooks configured −5A hook runs commands with the agent's privileges
Anything that could not be establishedCosts what its worst grant would have cost
Incomplete coverage −10

Score bands give the severity, except where one fact should outrank the arithmetic. Unattended unrestricted shell is always critical, so is unrestricted shell with reachable credentials, and so is an agent wired to a server its own scan graded D. And as with the trust grade's A, incomplete coverage can never be low risk.

Missing evidence never scores better than the bad case it might be hiding. A capability that could not be read is charged as though it were unrestricted, because anything else would let a machine score better by being harder to read.

Confidence is reported beside the score, never folded into it. A 90 established from complete evidence and a 90 with half the config unreadable are not the same claim.

The score is computed in one place and read everywhere. The CLI prints it, the API stores it and the dashboard renders it; none of the three recomputes it.

Attack paths

A path is a chain from an agent to something it can reach, and it appears only when every step was read out of a configuration:

Claude Code  →  Shell  →  aws  →  aws credentials file  →  the AWS account these credentials belong to

Each step lists what established it: the permission rule, the sandbox setting, the credential file that exists. The whole chain is repeated as a table, because a row of chips is not something everyone can read.

What is deliberately absent matters more than what is present. An agent that might reach the network, that might reach a cloud, that might reach production is three maybes chained together. It looks like a finding, is not one, and a product that publishes them teaches people to ignore it.

The clearest consequence: an agent allowed only Bash(npm run *) with AWS credentials sitting on disk produces no path, because nothing establishes that aws is among the commands it may run. If the allowlist names it (Bash(aws s3 *)), the path appears, at high severity rather than critical.

The same rule elsewhere:

SituationPath?
Unrestricted shell, no credentials foundNo
Credentials found, no shell at allNo
Shell level could not be establishedNo
MCP server auto-approved, never scannedNo: auto-approval alone is not evidence of anything
MCP server graded D, but still asks firstNo: there is a human in the way
MCP server auto-approved and graded DYes

An empty Attack paths page means no evidenced path was found. It is not a guarantee that none exists, and the page says so.

Credentials: presence only

Aevrin records that a credential is reachable, never what it is.

{ "kind": "github_token", "present": true, "source": "environment", "location": "GITHUB_TOKEN" }

The value is never read, never stored and never transmitted. Knowing a GitHub token is within reach of an agent that also has shell access is the finding; the token itself adds nothing to it, and would turn a posture report into a breach if it leaked.

Coverage

coverage says what was established and what was not. A configuration file that exists but will not parse is reported as unreadable, never treated as absent: an unparseable config is not an empty one, the same rule the MCP scanner applies to a stage whose tools did not run.

MCP trust grade

A scan produces a number. The number answers "how many problems does this have"; it does not answer the question you have in front of an install prompt, which is whether to let the thing run. The grade answers that, and the score stays underneath for the detail.

GradeMeaningRecommended action
ATrustedAllow
BGenerally safeAllow with caution
CCautionRequire approval
DHigh riskBlock or isolate

The recommendation is a recommendation. Aevrin does not enforce it, and does not change any configuration on the strength of it.

Grades are derived from the findings a scan already produced (the same scanners, severities and OWASP MCP categories), so a grade can never disagree with the scan it came from. It appears on a scan of an MCP target:

MCP trust:  C  Caution   recommended: require approval
  + 15  1 high-severity finding
  +  5  1 medium-severity finding
  +  8  authentication could not be established
  + 10  reachable over plaintext HTTP

Every letter arrives with the factors that produced it. Two rules override the arithmetic, because a weighted total can otherwise average away the one fact that mattered:

  • A critical finding is always D. No combination of good properties offsets it.
  • Unauthenticated command execution is always D. That is remote code execution with a friendlier name.

And one rule protects the top of the scale: incomplete coverage can never be A. A is a claim that the evidence is good, which requires evidence to exist. The clean-looking part of a scan that did not finish is exactly what was not established.

Aevrin's grading is its own. It is not derived from, and does not claim to match, any other project's methodology.

One server, however many places it is configured

The same server reached from Claude Code on one laptop and Codex on another is one row with two installations, not two servers. Without that, the page shows the same thing repeatedly under whatever local name each person gave it, and no count on it means anything.

Servers are matched on what the configuration actually pins down:

SignalExampleConfidence
URLhttps://mcp.context7.com/mcpHigh
npm or PyPI packagenpx -y @modelcontextprotocol/server-githubHigh
Docker imagedocker run --rm ghcr.io/acme/mcp:1High
The whole command linepg-mcp --port 5432Medium
Only the name someone typedpostgresLow

Launcher noise is not identity: npx -y pkg and npx pkg are the same server, and so are /usr/local/bin/npx and npx.cmd. A different path or port in a URL is a different server.

Uncertain identity is recorded, never resolved. A server Aevrin can only identify by its local name is marked Identity uncertain and is never merged with anything else, because merging two unrelated servers attaches one server's findings to the other, a worse failure than listing one server twice. Expanding a row shows every installation: the agent, the device, the scope, the project, the local name it was given there, and whether it is enabled.

Grades on the MCP servers page

A grade appears there only when a scan of that server actually ran, matched on the same identity above, so a scan of https://x/mcp/ correlates with a config of https://x/mcp, and nothing looser. A server with no matching scan reads Not scanned, and the letter is absent rather than optimistic.

Servers launched over stdio (a command on your machine, like npx -y @modelcontextprotocol/server-github) have no URL for Aevrin to reach. They show a grade once you scan them with aevrin agent scan --scan-servers --upload: that scan's target is the package's launch command, which matches the config entry it came from. Until then they read Not scanned.

Usage and plan limits

A posture upload spends from the same monthly credit pool as every other action, on the same rolling month. Two things gate it:

Credits. One upload costs 5 credits, per aevrin agent scan --upload, however many agents that upload reported. Claude Code and Codex found by the same command are one upload, not two: billing two would charge you for the tool being thorough.

Monitored devices. The number of machines whose posture is tracked. A machine already being tracked keeps reporting even at the limit, so a plan change can never silently stop watching a device that was already being watched. Only a machine Aevrin has never seen is refused.

Free covers 1 monitored device; Pro covers up to 10. See Concepts → Credits.

Both appear on the billing page. When the device limit is reached, the refusal says the machine is not monitored, never that it is fine:

This device is not monitored: your plan covers 1 device(s) and 1 are already tracked.
Nothing has been assessed about this machine, which is not the same as it being low risk.

That distinction is the same one the scanner makes between an incomplete scan and a clean one, and it matters more here: a fleet page showing three green machines and silently omitting a fourth is worse than one that says it cannot see the fourth.

Limitations

  • Only Claude Code and Codex are discovered today.
  • Discovery reads configuration. An agent's actual runtime behaviour is not observed.
  • MCP servers found on a machine are not automatically scanned; a trust grade requires a scan of that exact target, and servers launched over stdio have no target to scan.
  • Aevrin cannot change configuration on your machine from the dashboard. Adding, removing or disabling an MCP server happens on the machine itself.
  • A device beyond your plan's allowance is not monitored at all. It is absent from every page rather than shown as low risk.
  • Everything described here is scoped to your own account. Nobody else sees the devices you reported, and only you can remove them.
  • Change history is not kept. Each upload replaces the previous snapshot for that machine.
  • Devices are not connected to Aevrin. A snapshot is as current as the last time you ran the command there, and the dashboard shows that timestamp for exactly that reason.