secretgate
v0.4.1
Published
Offline secret guardrail for AI coding agents. Redacts credentials before they reach the model, rehydrates them locally. No account, no cloud, no telemetry.
Downloads
1,028
Maintainers
Readme
secretgate
Keep credentials out of your AI coding agent — without breaking your flow.
You paste a stack trace into Claude Code. It has your database URL in it. That URL is now in someone else's logs, and rotating it is your afternoon.
secretgate sits between you and the agent. It finds the credential, swaps it for a stable placeholder, lets the agent do its work, and puts the real value back locally before anything touches disk. The model never sees the secret. You never lose your train of thought.

Note what survived that round trip: the agent's edit (db.internal became
db.prod.internal) is kept, and all three credentials come back. The agent
could still reason about the file — it knew that was an AWS key and a Postgres
password — it just could not read either one.
First — which agent are you using?
| Your agent | Capability | What you get |
|---|---|---|
| Claude Code | redact | Denies reads of .env, redacts prompts, restores values afterwards |
| Gemini CLI | redact | As above, and rewrites tool arguments in place |
| Antigravity | redact | Denies reads and commands, restores values afterwards |
| Cursor | redact | As Claude Code. Beta — verify with doctor |
| GitHub Copilot | redact | Denies and rewrites, but has no post-tool event, so placeholders are not auto-restored |
| Windsurf | block only | Denies reads and commands. Nothing is redacted or restored |
| Codex | partial, block only | Bash commands only. File reads are not covered, and hooks need a feature flag |
| Aider | no hooks | Aider has no hook system. Use secretgate lock |
| Anything else | — | Use secretgate lock |
Capability is not a detail. On a block-only host there is no mechanism to
swap a secret for a placeholder, so a finding becomes a refusal instead. If you
are on Windsurf or Codex, do not paste a .env into a prompt expecting it to be
handled — it will not be. secretgate doctor prints the capability next to each
agent so this is never a guess.
Wiring a hook is also not the same as it working. init never claims you are
protected; only doctor can tell you that, and only after the hook has fired at
least once.
For any unsupported agent
lock needs no cooperation from the agent at all:
secretgate lock . # .env values become placeholders; real ones move out of the project
# ... let the agent do its work ...
secretgate unlock . # values come back, byte for byteNew here? docs/GETTING-STARTED.md is a worked example you can paste into a terminal.
Quickstart
Three commands. About a minute.
1. Install and wire it up
npm install -g secretgate
secretgate initinit finds the agents you already have, adds the hooks, and prints what it
changed. It backs up every file it touches, and secretgate uninstall puts them
all back exactly as they were.
secretgate init
+ Claude Code wired (backup: ~/.claude/settings.json.secretgate-backup-…)
+ git pre-commit hook created
+ secretgate.yml created
done — 2 hook(s) wired2. Check it is actually running
Use your agent normally for a minute, then:
secretgate doctoragents
Claude Code firing, last seen 12s ago
UserPromptSubmit 12s ago 4x
PreToolUse 12s ago 9xfiring is the word that matters. If it says wired, but has never fired,
the hook is configured but your agent is not calling it — so you are not
protected. Nothing else in the output means anything until that says firing.
3. That's it
There is no step 3. From here on it works on its own:
- Your agent is refused when it tries to read
.env,~/.aws/credentials,id_rsa, or runcat .env/printenv. - Any credential that reaches a prompt anyway is swapped for a placeholder like
SECRETGATE_AWS_KEY_6B8A, and the real value is put back locally when the agent writes a file. git commitis blocked if you are about to commit a credential.
Where do I put my .env?
Leave it exactly where it is, in the project root. Moving it achieves nothing —
the protection is not that the file is hidden, it is that the read is
intercepted. .env and .env.example can sit side by side in the same folder:
the agent is refused the first and allowed the second.
Try it without installing anything
If you would rather see it work before wiring it into your editor:
printf 'AWS_ACCESS_KEY_ID=AKIA4KTNQ7VZL2WXMP3D\n' > /tmp/demo.env
npx secretgate filter < /tmp/demo.envAWS_ACCESS_KEY_ID=SECRETGATE_AWS_KEY_6B8ATwo useful extras
secretgate scan . # find credentials already in your code
secretgate fix . # move them into .env (dry run; add --write to apply)If your agent is not supported
See the capability table above. Aider and
anything not listed have no hook to attach to, so use secretgate lock — it
needs no cooperation from the agent.
Why this and not the alternatives
| | secretgate | GitGuardian ggshield | Cycode AI Guardrails | agent-guard |
|---|---|---|---|---|
| Account required | no | yes | yes | no |
| Works offline | yes, enforced in CI | no | no | yes |
| Telemetry | none | yes | yes | none |
| Redact and restore | yes | block only | block only | block only |
| Agents covered | 7 with hooks, plus lock for any agent at all | several | several | 2 |
| Live-credential verification | no | yes | yes | no |
| Org policy / dashboards / incident workflow | no | yes | yes | no |
| Rule count | 55 hand-written (+ gitleaks import) | thousands | thousands | ~20 |
| Runtime dependencies | zero | many | many | none (shell) |
| Licence | Apache-2.0 | commercial | commercial | MIT |
Be honest about the trade: if you need to know whether a leaked key is still live, need an audit trail for a compliance team, or need to push policy to 500 engineers, buy GitGuardian or Cycode. Those are real capabilities we do not have and are not building. secretgate is for the individual developer or small team who wants a guardrail that works on a plane, requires no signup, and does not turn every secret into a hard stop.
Precision over recall, deliberately
A scanner that cries wolf gets uninstalled, and an uninstalled scanner has zero recall. So every design decision here favours precision:
- Entropy findings never block by default — they warn. Entropy alone is the single largest source of false positives in this whole category of tool.
- Placeholder words are matched as delimiter-separated tokens, never substrings.
Substring-matching
testswallows roughly one real key in a thousand. - Findings in
test/,fixtures/and*.test.*are downgraded, not dropped — fixtures do occasionally hold a live key. secretgate baselinehashes everything currently flagged so an existing codebase does not greet you with 400 findings on day one.
Current corpus results
corpus 138 true positives 118 hard negatives 60 mixed prompts
precision 100.00%
recall 100.00%
latency p99 1.2ms on a 10KB prompt (budget: 50ms)A tool scoring 100% on its own corpus is not evidence of anything — every scanner does. The corpus is the artifact worth reviewing, not the score.
The 118 hard negatives are hand-written and live in
scripts/gen-corpus.ts, each one labelled with why it
is not a secret. Go read them and decide whether they look like your codebase.
CI fails below 98% precision, and test/scores.json is committed so any change
in detection quality shows up as a diff in review.
The corpus JSON itself is generated rather than committed (npm run corpus),
because it contains ~200 realistically shaped fake credentials and every push
protection scanner on earth correctly objects to them. That the fixtures trip
real scanners is a reasonable sign they are realistic.
If secretgate misses something in your codebase, or flags something that is not a secret, open an issue with a sample — that is the most useful contribution anyone can make here.
Install
npm install -g secretgate # or use npx, no install needed
secretgate initRequires Node 20+. Prebuilt binaries are on the releases page for machines without Node.
Zero runtime dependencies. npm install secretgate pulls nothing else in —
the whole tool is one 66 KB file. For something that handles credentials, an
empty dependency tree is a feature: there is no supply chain to audit and
nothing that can change under you. The YAML config is read by a ~90-line parser
covering exactly the documented schema rather than a general-purpose library.
Commands
secretgate init detect installed agents and wire every hook
secretgate scan [path] scan a file or directory
secretgate fix [path] move hardcoded secrets into .env (--write to apply)
secretgate filter stdin -> stdout, redacted (--rehydrate to reverse)
secretgate baseline accept every current finding
secretgate doctor check what is wired up and whether it is firing
secretgate uninstall restore every config file we touched
secretgate rules list detection rules
secretgate vault inspect the placeholder store (--clear to wipe)
secretgate mcp-proxy -- <cmd> guard an MCP server's stdio in both directionsfix — because finding it is only half the job
Every other tool in this space stops at "there is a secret on line 12" and leaves you to do the actual work. That is the moment people reach for a pragma instead, and a suppressed finding is a secret that is still in the repo.
secretgate fix . # dry run, shows the diff
secretgate fix . --write # apply it- databaseUrl: "postgres://svc:[email protected]:5432/orders",
+ databaseUrl: process.env.DATABASE_URL,The value goes into .env at the project root, the key goes into
.env.example with an empty value, and you get a warning if .env is not
gitignored — moving a secret into a committed file is not an improvement.
Note it moves the whole string literal. Replacing only the matched password
would produce "postgres://svc:process.env.X@host/db", which still compiles and
silently never resolves. Template literals with ${interpolation} and values
that are not inside a string are reported and left alone rather than guessed at.
JS/TS, Python, Ruby, Go, PHP and shell.
doctor — configured is not the same as working
agents
Claude Code wired, last fired 4m ago
UserPromptSubmit 4m ago 31x
PreToolUse 4m ago 112x
Cursor wired, but has never fired — start a session and check againHook APIs move. An event gets renamed, your hook stops being called, and you
keep pasting .env files into prompts believing you are covered. Every hook
invocation stamps a local file with the event name and timestamp — no content —
so doctor can tell you the difference between "the config looks right" and
"this is actually running".
Custom rules
Every company has credential formats nobody outside it has heard of. Without these, secretgate is a tool an individual can use and a team cannot.
rules:
custom:
- id: acme-service-token
provider: acme
description: ACME internal service token
regex: "ACME-SVC-[A-Z0-9]{32}"
prefilter:
- "ACME-SVC-"
confidence: highRules that fail to compile are reported rather than silently dropped, and one that backtracks catastrophically is rejected outright — left in, it would hang the scanner on every prompt and you would blame the agent.
MCP servers
An MCP server sits at the end of a pipe the agent hooks never see. Tool
arguments go straight from the model to the server and results come straight
back, so a filesystem server reading .env leaks past everything else here.
secretgate mcp-proxy -- npx -y @modelcontextprotocol/server-filesystem /srvRelays stdio JSON-RPC in both directions, redacting arguments on the way out and results on the way back. stdio rather than HTTP deliberately: a network client would have to live in the one part of this codebase that promises never to make a network call.
pre-commit
repos:
- repo: https://github.com/jvlabsai/secretgate
rev: v0.1.0
hooks:
- id: secretgateThe filter is the universal escape hatch — it makes secretgate work with tools nobody has written an adapter for:
cat prompt.txt | secretgate filter | your-agentExit code 0 clean or redacted, 2 blocked.
Configuration
secretgate.yml, found by walking up from the working directory:
mode: redact # redact | block | warn
entropy:
enabled: true
threshold: 4.0
action: warn # blocking on entropy alone is how a scanner gets uninstalled
rules:
disable: [] # rule ids or whole providers
allowlist:
paths: ["**/fixtures/**"]
patterns: ["EXAMPLE_.*"]
vault:
persist: false # in-memory only, so a crash cannot leave secrets on diskPer-line opt-out, in any comment syntax:
API_KEY = "AKIA4KTNQ7VZL2W…" # secretgate:allowHow the vault works
Outbound, each finding becomes SECRETGATE_<PROVIDER>_<KIND>_<HMAC4>:
- Stable within a session — the same secret always maps to the same placeholder, so a multi-turn conversation stays coherent for the model.
- Uppercase and underscore only — no shell, JSON, YAML or language parser will quote, escape or reflow it.
- Suffix is an HMAC under a per-session random key, so nothing about the secret leaks through the placeholder.
Inbound, placeholders are swapped back before bytes reach disk. Anything that looks like ours but is not an exact match is reported and left alone. If the agent truncated or reformatted a placeholder, or invented one, secretgate warns rather than substituting — writing a real credential into a location nobody chose is worse than an edit that fails.
The vault refuses to render its contents through JSON.stringify,
console.log, util.inspect or a stack trace, and there is a test that greps
every rendering path for a known secret.
Redact mode stores the mapping on disk, at ~/.secretgate/vault.json, mode
0600, entries expiring after 12 hours. It has to: agent hooks run one process
per event, so a purely in-memory vault has exited before the agent's edit comes
back to be restored. Inspect it with secretgate vault, wipe it with
secretgate vault --clear.
That is a real trade and SECURITY.md argues it honestly rather
than glossing it. If you would rather no secret ever touched disk, use
mode: block.
What is not built yet
Being straight about it rather than letting you find out:
- Adapters beyond Claude Code are written but unverified against a live
host. Cursor, Gemini, Antigravity, Copilot, Windsurf and Codex were built to
each host's documented hook contract, which is young and moves. They all read
defensively and fail open.
doctortells you whether one is actually firing — precisely the case the heartbeat exists for. Reports of a hook that does not fire are the most useful issue you can open. - Aider has no hook system, so there is nothing to attach to. It is lock-only and always will be until Aider adds one.
- Encryption at rest for the vault — the store is
0600and short-lived but not encrypted, because a key stored beside the ciphertext is theatre. Doing it properly means an OS-keychain dependency, which conflicts with the zero-runtime- dependency guarantee. Open question, honestly flagged. - The gitleaks rule import —
scripts/sync-rules.tsworks and emits a committedgenerated.ts, but the shipped ruleset is the 55 hand-written rules.
Threat model
Read SECURITY.md before relying on this for anything serious. The short version: pattern-based detection is defence in depth, not an adversarial boundary. secretgate raises the cost of an accident. It will not stop an agent that is actively trying to evade it, and it is not a vault, a rotation tool, or a substitute for scoping your credentials properly.
Contributing
The most valuable contribution is a false positive. If secretgate flags
something in your codebase that is not a secret, open an issue with a sample —
that sample goes straight into test/corpus/hard-negatives.json and CI keeps it
from ever regressing.
npm ci
npm test # unit tests
npm run bench # corpus precision/recall
npm run ci # everything CI runsLicence
Apache-2.0. Detection rules derive in part from Gitleaks (MIT) — see NOTICE.
