Skip to main content
Glama

M8ven Score mcptry

Catch leaked keys and made-up packages before they ship.

yof (yof by sōtem) is yof on your laptop: a small command-line check for apps built with AI assistants (and apps built any other way). It finds secret keys sitting in your code and dependencies that don't exist or imitate popular ones, before they reach a commit, a pull request or production.

It is rules only: exact patterns, fast, and private. It runs on your machine, needs no account, and never sends your code anywhere.

Try it in 30 seconds

npx @yof/cli check

That checks the current folder. Node 20 or newer. Nothing to configure.

yof checked 2 files (profile: balanced)

Keys
  src/config.ts:1  Stripe live secret key  sk_live_FwIw••••••••  (fix now)
  A key that was ever committed or shipped counts as leaked: rotate it in the provider's dashboard,
  then keep it on the server only (an environment variable, not the code).

Packages
  package.json  reqeusts-helper (npm)  (fix now)
    This package doesn't exist on npm. AI assistants sometimes invent package names, and attackers register those names with malware. Check the spelling or remove it.
  package.json  expres (npm)  (fix this week)
    This name is one or two letters away from the popular package express. Make sure you meant this one and not express.

Found 3 problems: 2 to fix now, 1 to fix this week.
Want this on every pull request? https://yof.io

yof never prints a key in full. You see the prefix and four characters, enough to know which key it is.

Install it if you use it often:

npm install --save-dev @yof/cli
npx yof check

Related MCP server: guardvibe

Check every commit (pre-commit hook)

npx @yof/cli hook install

This adds .git/hooks/pre-commit, which runs yof check --staged before each commit. In staged mode yof reads exactly what is about to be committed (the staged content, not your working copy), and checks only the dependencies the commit adds. If something must be fixed now, the commit stops and yof says why.

  • Remove it with npx @yof/cli hook uninstall.

  • Skip it once with git commit --no-verify.

  • If you already have a pre-commit hook, yof leaves it alone and tells you the one line to add to it.

Check every push (GitHub Action)

# .github/workflows/yof.yml
name: yof
on: [push, pull_request]
permissions:
  contents: read
jobs:
  yof:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: yof-dev/yof@v0.2.0
        with:
          path: .          # optional, default .
          offline: false   # optional, true = keys only, no registry lookups

The job fails when yof finds something to fix now, so it shows up as a red check on the commit or pull request.

Settings: .yof.yml

Optional. Put it in the root of your repository. It is the same file yof Guard reads on GitHub, so one file covers both.

profile: balanced    # relaxed | balanced | strict

ignore:
  - fixtures/        # a folder
  - docs/examples.md # a file
  - "**/*.snap"      # a glob: * stays inside one folder, ** matches any depth

Profile

Shows

Fails (exit 1) on

relaxed

fix now

fix now

balanced (default)

fix now, fix this week

fix now

strict

everything, including public keys that are fine

fix now, fix this week

--profile strict on the command line overrides the file. yof also skips node_modules, .git, dist, build, .next and similar folders, lock files, binary files, files over 1 MB, and what your root .gitignore lists (read simply: plain names, folders and globs; ! lines are not applied).

Accepted risks: .yof/accepted.json

Some findings are fine on purpose, like a revoked test key in a fixture. Accept one from your editor (VS Code: the quick fix "yof: Accept this risk…") with a one-sentence reason, and commit the file it writes:

{ "version": 1, "accepted": [ { "rule": "secret.stripe_test", "file": "tests/pay.ts", "fingerprint": "…", "reason": "A test key in a fixture", "acceptedBy": "Ada", "acceptedAt": "2026-11-07T10:00:00.000Z" } ] }

yof check still prints an accepted finding, under "Accepted risks", with the reason, who and when; it no longer counts as open or fails the check. The fingerprint covers the rule, the file and the finding's line, so if that line changes the finding is open again. Remove the entry to reopen it. yof's signed reports list these entries in their "Accepted risks and why" appendix.

What it checks

Keys (exact shapes only, so a random hash never raises an alarm):

Fix now

Fix this week

Fine, public by design (shown in strict)

Stripe live secret and restricted keys, Stripe webhook secrets

Stripe test secret keys

Stripe publishable keys

OpenAI, Anthropic, Groq, Hugging Face, Replicate

An AWS access key ID on its own

Firebase web keys

AWS access key with its secret next to it

Browser-visible settings named like a secret (VITE_, NEXT_PUBLIC_, REACT_APP_, EXPO_PUBLIC_ … SECRET / PASSWORD / PRIVATE)

Supabase anon keys

Supabase service-role keys (the JWT is decoded, not guessed)

GitHub tokens, Slack tokens and webhook URLs

Resend, SendGrid, Mailgun, Twilio

Private keys (BEGIN … PRIVATE KEY)

Placeholders such as sk_live_xxxxxxxx or sk_live_your_key_here are skipped, so docs and examples stay quiet.

Packages in package.json, requirements*.txt and pyproject.toml:

  • Made up: the name does not exist on npm or PyPI. AI assistants sometimes invent package names, and attackers register those names with malware. (fix now)

  • Lookalike: one or two letters away from a popular package (expres, reqeusts) and not well established itself. (fix this week)

  • Brand new: first published in the last 14 days. (fix this week)

  • Known advisory: an exact pinned version with a published advisory in the OSV database. High and critical are fix now, the rest fix this week.

If a registry does not answer, yof says so and claims nothing about that package.

What it does not do: it does not read git history, scan your running website, check your database rules, or review code logic. Those live in the yof app and Guard (below).

Attack your own app: yof attack

yof attack tests YOUR OWN running app the way an attacker would, with two test accounts you create for it (A and B, plus an admin account if you have one). It reports only what it saw happen twice, and turns each finding into a regression test.

npx @yof/cli attack http://localhost:3000
npx @yof/cli attack --mode light

What it tries:

Group

What it does

auth

Opens each signed-in route with no sign-in and with a broken token

tenant

Account A creates a test record, account B reads it by id, finds it in a list and changes it

rbac

Opens each admin route as a normal member

ai

Your own assistant route: asks for its hidden instructions, types a command into a message, hides one in a record it reads (an HTML comment, a link title, a "system" field), asks it for account B's record, to rename account B's record, and for admin data; checks whether account B's record steers account A's general answers (RAG poisoning); asks it to answer with live HTML or a picture link, to drop its rules for one harmless word, for its internal addresses and tool list, and to send an email to a .invalid address; and, only with "limits": true, sends 10 tiny messages two at a time and asks for one very long story

surface

Open redirects, security headers, the sign-in cookie flags, and (when you turn it on) twelve wrong passwords in a row

Safe by design, and enforced in the code:

  • Only a host you proved is yours. localhost; a preview that GitHub reports as a successful deployment of this repository for the commit under test; or your live host with the proof token from your yof app (YOF_VERIFY_TOKEN, the same file, meta tag or DNS record the yof app uses).

  • Written consent per app, word for word in the config, naming the app.

  • Never production unless the config turns it on for this app. Then a light run (read-only) may go any time, and a full run only inside the window you set.

  • No destructive requests. GET and HEAD, plus POST only to the create, sign-in and assistant routes you name. PATCH, PUT and DELETE only touch records the run created itself, and those are deleted at the end, always.

  • Hard budgets: 300 requests and 110 seconds by default (at most 2,000 and 15 minutes), a steady pace, a 10 second timeout. When a budget runs out the run stops and says so, after deleting its test records.

  • Stop it any time. Ctrl+C (SIGINT) or SIGTERM, as an editor or CI sends on cancel: nothing new is sent, the test records the run created are deleted (within 10 seconds), it says what it deleted and anything it could not, and exits 130 or 143. A second Ctrl+C quits at once.

  • Mild, fixed assistant messages. The assistant probes are the same fixed texts every run (the OWASP Top 10 for LLM Applications 2025 families). None asks for harmful content: the jailbreak asks only for a harmless test word, picture links point at yof-canary.invalid and the email probe at an example.invalid address, which never resolve. Rules decide every finding, never a model. For internal details only the kind is kept (a private address, a connection string, a server path, a tool list), never the text.

  • Usage limits are opt-in. "limits": true on an ai entry adds a burst of 10 tiny messages and one request for a very long story, because it costs you a little model use. Without it, nothing of the kind is sent.

  • Your test accounts stay in your CI secrets. The config names environment variables, never a token or password, and nothing is stored by yof.

The config lives at .yof/attack.json:

{
  "app": "shop.example.com",
  "consent": "I own shop.example.com and allow yof attack to test it and its preview builds with my own test accounts.",
  "accounts": {
    "a": { "header": "Authorization", "env": "YOF_ATTACK_A" },
    "b": { "header": "Authorization", "env": "YOF_ATTACK_B" },
    "admin": { "header": "Authorization", "env": "YOF_ATTACK_ADMIN" }
  },
  "routes": ["/api/me", "/api/notes"],
  "admin": ["/api/admin/users"],
  "objects": [{
    "create": { "path": "/api/notes", "body": "{\"title\":\"{{marker}}\"}" },
    "idPath": "id",
    "read": "/api/notes/{id}",
    "list": "/api/notes",
    "update": { "method": "PATCH", "path": "/api/notes/{id}", "body": "{\"title\":\"{{marker}}\"}" },
    "delete": "/api/notes/{id}"
  }],
  "ai": [{
    "path": "/api/chat", "body": "{\"message\":\"{{message}}\"}", "replyPath": "reply",
    "ingest": { "create": { "path": "/api/notes", "body": "{\"title\":\"{{document}}\"}" }, "idPath": "id", "delete": "/api/notes/{id}" },
    "limits": false
  }],
  "surface": { "redirectPaths": ["/login"], "rateLimit": false },
  "production": { "enabled": false, "window": "02:00-04:00" },
  "budget": { "requests": 300, "seconds": 110, "perSecond": 8 }
}

An account can also sign in with an email and password from your CI secrets: { "login": { "path": "/api/login", "body": "{\"email\":\"{{email}}\",\"password\":\"{{password}}\"}", "tokenPath": "token" }, "emailEnv": "YOF_ATTACK_A_EMAIL", "passwordEnv": "YOF_ATTACK_A_PASSWORD" }. Without tokenPath the session cookie is used.

Regression tests

With --write-tests, every confirmed finding becomes a test in your own test framework (Vitest, Jest, Playwright or pytest, detected from your repository; a node --test file otherwise). It replays the recorded attack and expects the opposite: the test fails while the attack still works and passes once it is fixed. No AI writes it. It skips itself unless YOF_ATTACK_TARGET is set, so your normal test run is unaffected.

On every pull request

# .github/workflows/yof-attack.yml
name: yof attack
on: [pull_request]
permissions:
  contents: read
  deployments: read
  pull-requests: write
  issues: write
jobs:
  attack:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: yof-dev/yof/attack@v0.2.0
        env:
          YOF_ATTACK_A: ${{ secrets.YOF_ATTACK_A }}
          YOF_ATTACK_B: ${{ secrets.YOF_ATTACK_B }}
          YOF_ATTACK_ADMIN: ${{ secrets.YOF_ATTACK_ADMIN }}

It waits for the preview deployment of the pull request, then attacks only what the change can reach: the routes of the files it changes (Next.js app and pages routes, Hono and Express route files, and the routes that import a changed helper). The comment says "partial run: N attack groups". When shared code changes (middleware, auth, permissions, database access, dependencies) or the mapping is unsure, it runs everything and says why. Pull requests from forks are skipped.

The comment lists what was found. Comment /patch apply on the pull request and the yof GitHub App commits the regression tests (and the security-header fix, when one applies) to the pull request's own branch. It works only for people with write access, never on a fork, never on the default branch, and never force-pushes.

Set YOF_HOOK_URL (the deploy hook address from your yof app) and yof attack --upload sends the masked findings to your yof app, where they show with your other findings.

Options

yof attack [url]          The address to test (or YOF_ATTACK_TARGET)
--config <file>           The attack config (default .yof/attack.json)
--mode <full|light>       light = read-only
--base <branch>           Pull request run: attack only what the change can reach
--write-tests             Write a regression test for every confirmed finding
--comment <file>          Write the pull-request comment to this file
--upload                  Send the masked results to your yof app (YOF_HOOK_URL)
--json                    Machine-readable output

Inside your AI builder: yof mcp

yof mcp runs yof as a local MCP server (stdio), so the AI that writes your code can check it before it says "done". It works with any MCP client that starts a local command. The easiest way to connect is "Connect your AI builder" in the yof dashboard, which writes the config for you. By hand:

claude mcp add yof -- npx -y @yof/cli mcp          # Claude Code
codex mcp add yof -- npx -y @yof/cli mcp           # Codex CLI

Cursor, VS Code, Windsurf and Claude Desktop take the same command in their MCP settings: npx -y @yof/cli mcp. Always use the scoped name @yof/cli: the unscoped npm name yof belongs to someone else.

The tools:

Tool

What it does

Where it runs

yof_check

Scan the project folder (or a folder inside it)

Your machine

yof_check_diff

Scan only the lines that changed (a diff you pass, or the working tree against the last commit, plus new files)

Your machine

yof_explain

What a rule means, why it matters, the safe fix

Your machine

yof_fix_prompt

Exact fix steps for one finding

Your machine

yof_verify

Re-scan one file and say fixed or not

Your machine

yof_findings

Open findings from your yof.io account

yof.io, needs YOF_API_KEY

yof_patch_status

Patch's open pull requests and their CI state

yof.io, needs YOF_API_KEY

yof_lessons

Notes from merged Patch fixes

yof.io, needs YOF_API_KEY

Every answer starts with a line that says it is untrusted data from the scanned project, so text inside your files is never read as instructions. Secret values always come back as a short prefix plus REDACTED. Limits: a diff is at most 1 MB and 300 files, a scan stops after 90 seconds, and paths must stay inside the project folder.

YOF_API_KEY is made by the yof dashboard for one connection and can be revoked there at any time. With it, the server sends one ping when it starts (so the dashboard can show that the connection works) and, for the three account tools, the tool name and its arguments (ids and rule ids). Never code or file contents.

Privacy

yof check runs on your machine. It reads files, matches patterns and prints the result. Your code, file names and keys never leave your computer. (yof attack sends requests only to the app you proved is yours, and to GitHub's API to read this repository's deployments.)

The only network calls of yof check are package-registry lookups: package names (and exact pinned versions) go to registry.npmjs.org, api.npmjs.org, pypi.org and api.osv.dev, and to no other host. Skip them with --offline, and yof makes no network calls at all. There is no telemetry.

Options

yof check [path]       Check a folder (default: the current one)
yof check --staged     Check only the files staged for the next commit
yof hook install       Run "yof check --staged" before every commit
yof hook uninstall     Remove that hook
yof diff               Check only what changed since the last commit (plus new files)
yof mcp [path]         Run yof as a local MCP server for AI builders
yof import <file> [path]  Read a findings.json and check each record's file and line here

--offline              Skip the package-registry lookups (keys only)
--json                 Machine-readable output (masked values only)
--format <name>        text | json | findings-json
--profile <name>       relaxed | balanced | strict
-v, --version          Print the version
-h, --help             Print help

Exit codes: 0 nothing to fix, 1 something to fix (per the profile), 2 usage or setup error.

With --json, every finding also carries why (what can happen, in plain words) and fixPrompt (what to change, ready to paste into the AI assistant that wrote the code, with the masked value only). The yof editor plugins show these as they are.

findings.json (Cloudflare's security-audit format)

yof check --format findings-json writes the findings in the findings.json format of Cloudflare's security-audit skill (MIT licensed, see NOTICE), so the tools that read it can read yof too. Every record has a verdict: confirmed when the exact evidence is in the folder (a key in a provider's format, a package the registry does not have, an installed version an advisory names) and needs_validation when it is likely but not shown (a name one or two letters from a popular package, a package only days old). Keys that are public by design are hardening notes, not findings, and are left out. Nothing yof check finds is rated critical: it never runs your code or reaches a live app, so "high" is the most.

yof import findings.json [path] reads a findings.json strictly (a record with an unknown field, a missing field, a wrong value or a severity above its demonstrated impact is named and left out, never repaired) and checks each record against the folder, read only: does the file exist inside the folder, and does the line exist. Nothing is run and nothing is sent. Exit 2 when the file does not match the schema, 1 when a record needs a look, 0 when only rejected records are left.

You can also use it as a library: import { scanText, checkPackages, runCheck } from "@yof/cli".

Want more?

yof is the security watcher for AI-built apps. This CLI is its fast, private, rules-based part. The rest lives at yof.io:

  • The yof app at yof.io watches your live app, explains every finding in plain words, says what the fix is, and keeps track of your app over time.

  • Guard reviews every pull request on GitHub and leaves one clear comment: github.com/apps/yof-by-sotem.

  • Patch, yof's AI agent, writes the fixes for you as a draft pull request.

Contributing

Issues and pull requests are welcome, especially new key rules with a documented shape. See CONTRIBUTING.md. Security problems: SECURITY.md.

License

Apache-2.0. See LICENSE and NOTICE. yof is published by Mondaily Ltd.

Available Tools

8 tools
yof_checkCheck the projectA
Read-onlyIdempotent
Inspect

Scan the project folder on this computer (or one folder inside it) for leaked API keys and for made-up, brand-new, lookalike or vulnerable packages. Use it for a full pass, for example before you say the work is done; after small edits use yof_check_diff, which reads only the changed lines. Files are read locally and never uploaded; only package names and versions go to npm, PyPI and OSV, unless offline is true. It follows .yof.yml and .gitignore, reads at most 20,000 files (stopped_early says so), looks up at most 500 dependencies and gives up after 90 seconds. Returns counts, must_fix_before_done and up to 100 findings with secrets redacted; an empty list means nothing was found within those limits.

Inputs: path, offline. Returns: more, next, counts, profile, +7 more

yof_check_diffCheck what changedA
Read-onlyIdempotent
Inspect

Scan only the lines that changed: a unified diff you pass in, or, with no diff, the working tree against the last commit plus new untracked files (needs git). Use it after each edit and before you say the work is done; use yof_check for a full pass that also covers unchanged files. Only added lines are checked for keys, and only dependencies added to a changed manifest are looked up on npm, PyPI and OSV; code never leaves this computer. Limits: 1 MB of diff, 300 files and 90 seconds. Returns counts, must_fix_before_done and up to 100 findings with secrets redacted; an empty list means nothing was found in the changed lines.

Inputs: diff, offline. Returns: more, next, counts, source, +5 more

yof_explainExplain a findingA
Read-onlyIdempotent
Inspect

Explain one yof rule in plain words: what it means, why it matters, how urgent it is and the safe fix. Use it to understand a finding from yof_check, yof_check_diff or yof_findings; use yof_fix_prompt instead for step-by-step instructions for one specific finding. Key rules (secret.) and package rules (package.) are answered on this computer without reading project files; any other rule id is asked of yof.io only when YOF_API_KEY is set, and is an error otherwise. Returns rule, name (or title from yof.io), severity, why and safe_fix.

Inputs: rule_id (required). Returns: why, name, rule, title, +3 more

yof_findingsFindings from your yof accountA
Read-onlyIdempotent
Inspect

List open findings stored in your yof.io account: problems yof found on your proven apps' live sites and connected GitHub repositories. Use it to see what yof.io already knows; it does not scan this computer (use yof_check or yof_check_diff for that), and yof_fix_prompt takes a finding id from here. Needs YOF_API_KEY; sends only these arguments to yof.io and returns results from yof's latest checks, most recently seen first. Returns findings (id, app, rule, title, severity, where, redacted value, first and last seen) and next; an empty list means no open findings, and apps not proven yours are left out.

Inputs: limit, app_id, severity. Returns: next, note, findings

yof_fix_promptFix instructionsA
Read-onlyIdempotent
Inspect

Write step-by-step instructions to fix one finding, with any secret shown only as a short prefix plus REDACTED. Use it after yof_check, yof_check_diff or yof_findings reports a finding; use yof_explain for background on a rule, and yof_verify once the fix is made. For a key rule pass file (and line), which yof reads locally to find the value; for a package rule pass package. A finding_id, or a rule this computer does not know, is sent to yof.io and needs YOF_API_KEY. Returns rule and instructions, plus file, line and still_in_file for a key finding.

Inputs: rule_id (required), file, line, package, +1 more. Returns: file, line, rule, package, +3 more

yof_lessonsLessons from merged fixesA
Read-onlyIdempotent
Inspect

List short lessons from Patch fixes merged in your projects: what was fixed, in which pull request, whether yof's next check confirmed it, and how to avoid it next time. Use it before writing similar code so the same problem does not come back; use yof_patch_status for pull requests still open. Needs YOF_API_KEY; sends only app_id to yof.io, which answers from stored data (up to 50 merged pull requests, most recently opened first). Returns lessons (repository, pull_request, merged, rule, note, avoid_next_time); an empty list means no Patch fix has been merged yet.

Inputs: app_id. Returns: lessons

yof_patch_statusPatch pull requestsA
Read-onlyIdempotent
Inspect

List Patch's open fix pull requests from your yof.io account, with the state of each one's CI checks. Use it to see whether a fix Patch proposed is waiting, passing or failing; use yof_lessons for fixes already merged, and yof_findings for problems with no pull request yet. Needs YOF_API_KEY; sends only app_id to yof.io, which answers from what it last stored (up to 50, newest first) without asking GitHub again. Returns open (repository, number, url, draft, rule, title, checks, failed_checks, opened) and a note; an empty list means Patch has no open pull request.

Inputs: app_id. Returns: note, open

yof_verifyVerify a fixA
Read-onlyIdempotent
Inspect

Re-scan one file on this computer and report whether a finding is gone. Use it right after fixing something yof_check, yof_check_diff or yof_fix_prompt pointed to; it reads only that file, so run yof_check_diff afterwards to cover the rest of the change. With no rule_id it looks for every key rule (secret.*); a package rule needs package and asks npm, PyPI and OSV unless offline is true. A missing file, or one over 1 MB, counts as fixed. Returns file, rule, fixed (true, false, or null when a registry did not answer), why, and for key rules the matches still there, redacted.

Inputs: file (required), offline, package, rule_id. Returns: why, file, rule, fixed, +3 more

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.7/5.0

    Scored across 8 tools

    Disambiguation5/5

    Each tool has a sharply distinct role: yof_check (full scan), yof_check_diff (changed lines only), and yof_verify (single file re-check) are explicitly separated by scope, and yof_findings is explicitly stated not to scan locally. yof_explain (rule background) vs yof_fix_prompt (step-by-step fix) vs yof_verify (post-fix confirmation) are also cleanly differentiated, as are yof_patch_status (open PRs) and yof_lessons (merged fixes).

    Naming Consistency5/5

    Every tool uses the same yof_ prefix plus a lowercase snake_case noun/verb phrase, with no camelCase or style drift. The check/check_diff, patch_status/lessons, and explain/fix_prompt pairs read as a predictable family.

    Tool Count5/5

    Eight tools is well-scoped for a scan-diagnose-fix-verify lifecycle, and each tool earns its place by covering a distinct step. Nothing feels redundant or padded.

    Completeness4/5

    The surface covers scanning (full, diff, single-file), explanation, fix instructions, verification, cloud findings, and Patch PR/lesson tracking, which is a coherent lifecycle. Minor gaps exist: there is no tool to dismiss/ignore a finding, initialize or edit .yof.yml config, or trigger Patch to open a fix PR, so a few lifecycle steps end in a dead end.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      B
      maintenance
      Security MCP server with 300+ rules for AI-generated code. Scans Next.js, Supabase, Clerk, Stripe, Prisma, Hono, GraphQL and 20+ modules. Zero config, runs locally.
      39
      2,157 npm
      6
      Apache 2.0
    • A
      license
      Not graded
      quality
      C
      maintenance
      Security scanner for MCP servers — vet an MCP before you wire it into an agent. Detects prompt-injection, credential exfiltration (via taint analysis), RCE, and supply-chain risks, and catches cross-server exfil chains no single server reveals. Zero-dependency local CLI, SARIF output, CI-gateable, no account.
      63 npm
      MIT