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
| What | Where 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
| What | Where 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_mode | Filesystem read | Filesystem write | Shell | Network |
|---|---|---|---|---|
read-only | Full: the whole filesystem, not just the workspace | None | Limited | None |
workspace-write | Full | Limited to the workspace | Limited | None unless sandbox_workspace_write.network_access = true |
danger-full-access | Full | Full | Full | Full |
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 scanAdd --project to include a specific project's configuration, and --verbose to list every file
that was read:
aevrin agent scan --project ./my-repo --verboseEvery 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-githubHow 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
denyrule 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: bypassPermissionsgrants every capability, and is called out.- An MCP rule is read like any other rule.
mcp__githubandmcp__github__*allow every tool on thegithubserver (full),mcp__github__create_issueallows one tool (limited), and the same rule underaskis ask. Adenyrule 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. enableAllProjectMcpServersmarks 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 page | Means |
|---|---|
| Safety score | The agent posture score, 0 to 100. Higher is safer. It starts at 100 and loses points for each reason listed. |
| What it can reach | The widest access in any settings file, not only the setting that wins. |
| Everything, Some, Only if you say yes, No, Could not tell | Levels full, limited, ask, none and unknown. Could not tell is counted as the most it could allow. |
| Gives access, Asks you first, Limits or blocks | A 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 --jsonReturns 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 --uploadThe 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:
| Page | What is on it |
|---|---|
| Agents | Every agent that has reported, its device, its posture score and every deduction behind it |
| An agent | Capabilities with their evidence, MCP servers, skills, plugins, hooks, permission rules, credentials in reach, and every file that was read |
| Devices | Every machine that has reported, rated by the least restricted agent on it |
| MCP servers | Every MCP server across every device, one row per server, with its scopes and trust grade |
| Skills | Every skill installed, one row per installation, with its scope and source file |
| Permissions | Every rule across every device, exactly as written, with the file it lives in |
| Attack paths | What 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:
| Number | Question it answers |
|---|---|
| MCP scan score | How 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 score | How much can this agent already do on this machine? |
| Blast radius | What 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_credentialsThe rubric, in one place so it can be argued with:
| Deduction | Why |
|---|---|
| Unrestricted shell −15, command-scoped shell −5 | Bash(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 −15 | Adds no capability; removes the check on every capability already there |
| Credentials reachable from a shell −15 | The blast radius. The credential is not the risk, the combination is |
| MCP servers auto-approved −5 each, capped at −15 | Their one human checkpoint is gone |
| A server graded D −20, graded C −8 | Only ever from a grade a scan actually produced |
| Hooks configured −5 | A hook runs commands with the agent's privileges |
| Anything that could not be established | Costs 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 toEach 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:
| Situation | Path? |
|---|---|
| Unrestricted shell, no credentials found | No |
| Credentials found, no shell at all | No |
| Shell level could not be established | No |
| MCP server auto-approved, never scanned | No: auto-approval alone is not evidence of anything |
| MCP server graded D, but still asks first | No: there is a human in the way |
| MCP server auto-approved and graded D | Yes |
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.
| Grade | Meaning | Recommended action |
|---|---|---|
| A | Trusted | Allow |
| B | Generally safe | Allow with caution |
| C | Caution | Require approval |
| D | High risk | Block 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 HTTPEvery 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:
| Signal | Example | Confidence |
|---|---|---|
| URL | https://mcp.context7.com/mcp | High |
| npm or PyPI package | npx -y @modelcontextprotocol/server-github | High |
| Docker image | docker run --rm ghcr.io/acme/mcp:1 | High |
| The whole command line | pg-mcp --port 5432 | Medium |
| Only the name someone typed | postgres | Low |
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.