Neon Guard Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Neon Guard Mcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Neon's official MCP server is amazing, but giving an LLM full admin access to your infrastructure is terrifying. neon-guard-mcp acts as a local security proxy, giving your AI agent the exact tools it needs to branch and test code, while completely air-gapping your production data and master keys.
.neon-guard-state.json). A matching name prefix alone is not sufficient.init_source: schema-only, so no production rows are ever copied into an agent-accessible branch.Claude Code (or any MCP client)
|
| stdio (JSON-RPC)
v
+---------------------------+
| neon-guard-mcp proxy |
| - holds NEON_API_KEY |
| - enforces policy |
| - tracks provenance |
| - rewrites / filters |
+---------------------------+
|
| HTTPS (Neon REST API v2)
v
console.neon.techThe LLM calls tools. The proxy validates every call against config and provenance state, then translates allowed calls into Neon API requests. The master key is only ever present in the proxy process environment.
Neon's official server maps MCP tools 1:1 to Neon REST API operations. This is intentional and useful — for a human developer who understands what they are authorizing. For an autonomous AI agent, it means the LLM can:
main / production.One prompt-injection attack, one confused-deputy mistake, or one overly-helpful model decision is enough to destroy production data or exfiltrate it.
This proxy takes the opposite position: default-deny, least privilege, explicit allowlist. The agent gets only what it needs to create isolated test branches and run migrations/tests against them. Every other API surface simply does not exist as far as the LLM is concerned.
Name prefix is NOT a security boundary.
It would be trivial for an attacker (or a confused model) to supply a branch name that happens to match the configured prefix. Authorization is instead gated on provenance: the proxy records every branch it creates in .neon-guard-state.json. A branch must appear in that record AND pass live server-side checks (not default, not protected, not the enforced parent branch) before the proxy will issue a connection string, delete it, or reset it.
The provenance store uses atomic file writes (write-then-os.replace) with mode 0o600 to prevent partial reads and limit filesystem exposure.
uvx neon-guard-mcp==0.1.0Pinning the version is important for security: it prevents a supply-chain update from silently changing proxy behavior.
pip install neon-guard-mcp==0.1.0Until neon-guard-mcp is published to PyPI, install it from a local checkout:
git clone https://github.com/your-org/neon-guard-mcp.git
cd neon-guard-mcp
uv sync # or: pip install -e .
uv run neon-guard-mcp # runs the server over stdioIn your MCP client config, point command at uv with args: ["run", "neon-guard-mcp"] (or uvx --from /abs/path/to/neon-guard-mcp neon-guard-mcp) instead of the published-package form below.
Copy the example config and edit it:
cp neon-guard.json.example neon-guard.jsonNote:neon-guard.jsonand.neon-guard-state.jsonare in.gitignoreand must never be committed. The example file (neon-guard.json.example) is safe to commit — it contains no secrets.
| Field | Type | Default | Description | |
|---|---|---|---|---|
allowed_projects | list[string] | required | Neon project IDs the proxy is permitted to act on. Any other project ID is rejected. | |
branch_naming_prefix | string | required | Prefix applied to all branches the proxy creates (e.g. "ai-dev-"). Must match ^[a-z0-9][a-z0-9-]*$. | |
enforce_parent_branch | string | required | Name of the branch all agent branches must be parented from (e.g. "main"). Prevents branching off an already-modified branch. | |
branch_data_mode | "schema-only" \ | "full" | "schema-only" | Controls Neon's init_source. "schema-only" copies only DDL — no rows. Change to `"full"` only if you have reviewed the security implications. |
branch_expiry_hours | integer \ | null | 24 | Hours after creation before Neon auto-deletes the branch. null disables expiry. Expiry also invalidates the embedded credential in any connection string issued for the branch. |
allow_branch_deletion | bool | false | If false, the delete_test_branch and reset_test_branch tools are not registered at all. | |
max_ai_branches | integer | 10 | Maximum number of live agent-created branches per project. Prevents runaway branch creation. | |
connection.default_database | string | required | Database name used when generating connection strings. | |
connection.default_role | string | required | Postgres role used when generating connection strings. Should be a dedicated least-privilege role (see Recommended Neon Setup). | |
connection.pooled | bool | true | Whether to request a PgBouncer pooled connection URI. | |
disabled_tools | list[string] | [] | Tool names to omit from the MCP schema entirely. Useful for further restricting the agent's surface. |
All tools are read-only or scoped to branches the proxy created. No tool can touch production or untracked branches.
| Tool | What it does | Guard |
|---|---|---|
list_safe_projects | Lists Neon projects filtered to allowed_projects. | Intersection with server response; unlisted projects are invisible. |
list_safe_branches | Lists branches for an allowed project, annotated with created_by_guard. | Project must be in allowed_projects. |
create_test_branch | Creates a new branch from enforce_parent_branch using the naming prefix. | Project allowed; parent verified server-side; max_ai_branches enforced; name uniqueness checked; branch_data_mode and expires_at applied. |
get_branch_connection_string | Returns the connection URI for a guard-created branch. | Project allowed; branch must be in provenance store; branch must not be default, protected, or the enforced parent. |
get_branch_status | Returns branch state metadata. | Project allowed; branch must exist in the project. |
delete_test_branch | Deletes a guard-created branch. | Only registered if allow_branch_deletion: true; provenance + server-side safety checks; branch must have no children. |
reset_test_branch | Restores a guard-created branch to its parent's current state. | Only registered if allow_branch_deletion: true; provenance + server-side safety checks. |
.mcp.json integrationAdd this to your project's .mcp.json (or ~/.claude/mcp.json for global use):
{
"mcpServers": {
"neon-guard": {
"command": "uvx",
"args": ["neon-guard-mcp==0.1.0"],
"env": {
"NEON_API_KEY": "${NEON_API_KEY}",
"NEON_GUARD_CONFIG": "/absolute/path/to/neon-guard.json"
}
}
}
}Important: Theenvblock is passed only to theneon-guard-mcpsubprocess. Claude Code does not forward it to the LLM context.NEON_API_KEYnever appears in any tool call, tool result, or conversation message.
Note the "${NEON_API_KEY}" reference: Claude Code expands ${VAR} (and ${VAR:-default}) from your host environment at server-launch time, so the literal secret stays out of the config file. Use an absolute path for NEON_GUARD_CONFIG to avoid ambiguity about the working directory at startup.
Claude Code passes NEON_API_KEY to the proxy as a process environment variable when it launches the server — that is the only way the key reaches the proxy, and it never reaches the model. But Claude Code does not encrypt this value anywhere. How you supply it is up to you; here are the options, most secure first.
⚠️ Myth: "claude mcp add --envstores the key encrypted in~/.claude.json." False. It is stored as plaintext JSON. Treat anything you put in a config file or~/.claude.jsonas readable by anyone (or anything) with access to that file.
1. Secrets manager (recommended). The literal key never lands in any file — it lives in your vault and is injected into the environment only at launch. With the 1Password CLI, reference the key with an op:// secret reference and let op run resolve it:
{
"mcpServers": {
"neon-guard": {
"command": "op",
"args": ["run", "--", "uvx", "neon-guard-mcp==0.1.0"],
"env": {
"NEON_API_KEY": "op://Private/Neon/api-key",
"NEON_GUARD_CONFIG": "/absolute/path/to/neon-guard.json"
}
}
}
}The same pattern works with aws-vault exec, vault, doppler run, etc. — wrap the launch command, keep the secret in the manager.
2. Reference a host env var via `${...}` expansion. Keep the real value in your shell environment (sourced from an uncommitted file or your secrets manager) and reference it — the config stays committable because it holds no secret:
export NEON_API_KEY="napi_…" # from ~/.zshrc sourcing an uncommitted ~/.secrets, a vault, etc."env": { "NEON_API_KEY": "${NEON_API_KEY}" }3. Local scope via the CLI (fine for a personal machine — but plaintext at rest). --scope local (the default) writes to ~/.claude.json, which is not committed to git, so it's safe from your repo — but the value is plaintext on disk:
claude mcp add --scope local --env NEON_API_KEY="napi_…" \
neon-guard -- uvx neon-guard-mcp==0.1.0Never hardcode the literal key in a project-scoped `.mcp.json` — that file is meant to be committed, so the secret would land in your git history in plaintext. Use ${NEON_API_KEY} expansion or a secrets-manager wrapper there instead. (neon-guard.json and .neon-guard-state.json are already git-ignored, but the master key is never written to either of them — it only ever comes from the environment.)
default and protected flags and matching against enforce_parent_branch)..neon-guard-state.json.Connection strings are live credentials. When you call get_branch_connection_string, the returned URI contains a Postgres password. That URI enters the LLM context (it is the tool result). This is a fundamental constraint of how database connections work — the agent needs a credential to connect.
Mitigations applied by default:
neon_guard_ai in the example) should have only the privileges needed for migrations and tests, not superuser or neondb_owner.Operational posture: treat any branch that an agent has touched as potentially compromised. Deleting the branch (or allowing it to expire) rotates the credential. Do not reuse agent branches for sensitive data.
Log redaction is a best-effort backstop, not the primary control. The proxy never passes the master key or a connection URI to a log, an error message, or stdout, and it scrubs known secret shapes (Neon keys, Bearer tokens, postgresql:// credentials, password= keyword forms) from every log record as defense-in-depth. Redaction is pattern-based, so the real guarantee is the "never log a secret" discipline above — do not add log lines that interpolate credentials and rely on the scrubber to catch them.
If your agent processes untrusted input (web pages, user-provided files, database content from a previous branch), an adversary could embed instructions that attempt to exfiltrate the connection string or manipulate the agent into calling reset_test_branch on the wrong branch. The schema-only default and provenance gating significantly reduce the blast radius, but they do not eliminate the attack surface entirely. Consider whether your agent's task requires access to get_branch_connection_string at all; if not, add it to disabled_tools.
Create a dedicated database role with limited privileges rather than using neondb_owner:
-- Run as neondb_owner on your Neon project
CREATE ROLE neon_guard_ai WITH LOGIN PASSWORD 'choose-a-strong-password';
-- Grant only what your migrations/tests need
GRANT CONNECT ON DATABASE neondb TO neon_guard_ai;
GRANT USAGE ON SCHEMA public TO neon_guard_ai;
GRANT CREATE ON SCHEMA public TO neon_guard_ai; -- needed for migrations
-- Do NOT grant SUPERUSER, CREATEROLE, or CREATEDBSet connection.default_role to "neon_guard_ai" in your config. This ensures that even if a connection string is leaked, the role cannot escalate privileges, create new roles, or access other databases.
MIT — see LICENSE.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.