Stop committing secrets.

Blocks credentials before they reach git, finds the ones you already leaked, and tells you exactly how to rotate them.

any editor runtime deps 0 tests 65 fails closed MIT
$ npm install -g keyguard-cli && keyguard install

01It blocks the commit

Protection lives in git hooks, not an editor plugin. So it works the same in Cursor, VS Code, PhpStorm, Vim, or a plain terminal, and your colleague doesn't have to install your editor to be protected.

zsh
$ git commit -m "wip"

โœ– BLOCKED: secrets detected

.env
  โœ– 1:14  GitLab Token  glpaโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข[26 chars]

1 blocking finding

Rotate these credentials

  GitLab
    โ†’ https://gitlab.com/-/user_settings/personal_access_tokens
    โ€ข Revoke the token in User settings > Access tokens (self-hosted: your own GitLab URL)
    โ€ข Create a replacement with a short expiry
    โ€ข Check the audit events for use of the token

To resolve:
  โ€ข Move the value into an environment variable and reference it
  โ€ข If it is a false positive, add // keyguard:allow on that line
  โ€ข If it was ever pushed, assume it is public and rotate it

02It tells you what's already exposed

Most scanners check the code in front of you. keyguard doctor audits the machine: plaintext tokens in your shell config, .env files one git add -A away from being committed, and every repository where git is quietly ignoring your hooks.

keyguard doctor
Key-Guard: prevent ยท detect ยท rotate

Machine audit
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

โœ” Global git hooks are installed
    hooksPath = ~/.keyguard/hooks

โš  5 repositories bypass the global hook
    These set their own core.hooksPath, so git ignores the global hook:
      ~/work/checkout-service  (.husky/_, Husky)
      ~/work/design-system     (.husky/_, Husky)
    Fix with: cd <repo> && keyguard install --repo

โœ– Plaintext credentials in ~/.zshrc
    line 155: OpenAI API Key sk-jRโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข[25 chars]
    Readable by any process running as you, including any AI tool
    that reads your config files.

โš  1 env file is not gitignored
      .env
    Not committed yet, but `git add -A` would commit it.

1 critical issue ยท 2 warnings

03What you get

โ—† Never prints a secret

Findings carry a redacted preview and a hash fingerprint, never the raw value. Printing it in full would re-leak it into your shell history and CI logs.

โ—† Fails closed

If a scan crashes, the commit is blocked rather than waved through. A security tool that silently stops working is worse than none.

โ—† Honest about coverage

Husky repos override core.hooksPath and bypass global hooks. Key-Guard finds them, fixes them, and verifies git will really run the hook.

โ—† Rotation, not history rewriting

A committed credential is public. You get a per-service revoke checklist instead of a filter-repo command that breaks every clone.

โ—† Low noise, measured

Entropy findings never block. False-positive rate on real repositories is a release gate, not a hope.

โ—† Zero dependencies

Nothing in node_modules at runtime. A tool that reads your secrets shouldn't pull in a supply chain to do it.

04Commands

Five commands, each answering one question.

Command Answers
keyguard doctor What is exposed on this machine right now?
keyguard scan Does this project contain secrets?
keyguard history Did I already commit something, and how do I rotate it?
keyguard install Protect every repo on this machine
keyguard uninstall Remove the hooks, restore previous git config

05In CI too

The same engine, so CI and your machine never disagree about what counts as a secret.

โ—† GitHub Actions

- uses: L-ubu/Key-Guard@v0.1.1

Fails the job and writes a redacted findings table to the job summary. Emits SARIF for code scanning.

โ—† GitLab CI

include:
  - remote: 'โ€ฆ/keyguard.gitlab-ci.yml'

Three jobs: working-tree scan, merge-request range scan, and a manual full-history audit.

โ—† pre-commit framework

- repo: L-ubu/Key-Guard
  hooks: [{ id: keyguard }]

For teams already standardised on pre-commit.

SARIF upload to GitHub code scanning is free on public repositories but needs GitHub Advanced Security on private ones. Without it the job still fails and still prints the redacted table, and the docs say so rather than letting you discover it in a pipeline.

06It warns you before you even save

keyguard lsp is a language server, so any editor that speaks LSP underlines the secret as you type it. Same engine as the hooks, so the editor and the commit block never disagree with each other.

โ—† Move to .env

One quick fix replaces the literal with an environment lookup in the right syntax for the language, then appends the value to the nearest .env, creating it if it does not exist. The variable name is read from the assignment, so stripeKey becomes STRIPE_KEY.

โ—† Honest false positives

The other quick fix inserts keyguard:allow using a comment syntax the file actually accepts, so a Python file gets # and not //.

โ—† Still no dependencies

The JSON-RPC framing is written by hand. A process that reads every keystroke of your source is the last place to want an unaudited dependency tree.

init.lua
vim.lsp.config.keyguard = { cmd = { 'keyguard', 'lsp' } }
vim.lsp.enable('keyguard')

Neovim, Helix and Sublime work through the server directly. VS Code and Cursor have a dedicated extension that bundles it, currently installed from a locally built .vsix rather than a marketplace. JetBrains is tracked for v1.0. Whatever your editor, the git hooks are the part that actually blocks a commit.

07Why it's built this way

The decisions are the interesting part, so they're written down rather than buried in the code.

Bypass is explicit, not --no-verify

Use KEYGUARD_DISABLE=1 git commit. This exists so people reach for a documented, greppable switch instead of learning --no-verify, which skips every hook you have and becomes a habit.

Missing binary allows, broken binary blocks

If the scan errors, you're blocked. But if the keyguard binary is absent, hooks exit 0 with a warning, because otherwise uninstalling the package would brick every commit on the machine.

Staged content, not the working tree

The pre-commit hook reads from the git index. Stage a secret, then edit the file, and it's the staged version that gets committed, so that's the version that gets scanned.

History scans blobs, not diffs

git rev-list --objects yields each blob once regardless of how many commits contain it. Walking git log -p instead re-reads the same content hundreds of times.

JWTs are validated, not pattern-matched

The header is base64-decoded and parsed as JSON. Plenty of strings start with eyJ; far fewer decode to an object with an alg field.

Conventional defaults aren't leaks

ftp://anonymous:anonymous@host is documentation. Treating admin:admin as a breach turns a secret scanner into a weak-password linter, a different job and a much noisier one.

08Roadmap