PermLang
Know what your code can touch, before it ships.
A safety check for TypeScript projects. It notices when a change makes your code contact a new server, read or write files, use a secret, or change your database, and makes sure someone sees it before it's merged.
Get started · How it works · Reference · Live demo
Why it matters
Code gets written faster than ever, much of it by AI assistants. One added line can send customer data to an unknown website, and in a pull request with hundreds of changed lines, that line is easy to miss.
PermLang works like the permission prompts on your phone ("This app wants to use your location"), but for your codebase. It keeps an inventory of what every part of your code can reach. When a change reaches something new, PermLang flags it, in plain view, on the pull request.
How it works
The image at the top is a real pull request: it adds an enrich helper that
sends each lead's email and phone number to api.data-broker.io. The tests still
pass. PermLang fails the check, marks the line, and comments with what's new.
In words: this change makes the code send data to api.data-broker.io from
enrich, and handleLead can now trigger it too.
See the pull request.
What it watches
It also flags two things code review rarely catches:
- 🤖 Tools you give an AI model. A function registered as an AI tool (MCP, the Vercel AI SDK, the OpenAI SDK and OpenAI Agents, LangChain, LlamaIndex, Genkit, and others) can be triggered by whoever controls the model's input. PermLang lists every tool and what it can reach, and warns when a model could run commands, write data, send to any address, or read any file or secret it names.
- 🔒 Where secrets may go. A rule like "the Stripe key may only be sent to Stripe" fails a change where code that gets hold of the key can also send it somewhere else: another server, an email, a command, or a library PermLang can't see into. It follows the functions that read the key, not the key itself, so a key kept in a variable outside a function isn't tracked (details).
Going further: rules in the code
The inventory catches anything new. To also set limits, developers write a one-line note above a function saying what it's allowed to do:
/** @perm net(api.stripe.com), db.write(orders), env(STRIPE_KEY) */
export async function chargeCustomer(order: Order) { ... }
If a later change makes that function do more, the build stops and says exactly where, and how to fix it:
src/billing.ts:9:9 error PERM001: chargeCustomer calls fetch("https://data-broker.io/enrich", ...)
but its declared permissions do not include net(data-broker.io).
-> add net(data-broker.io) to @perm, or remove the call.
PermLang follows calls between functions and files, so moving the access into a helper function doesn't get around it.
Choose how strict
Start gentle and tighten up when you're ready.
Get started
In a TypeScript project:
npm install --save-dev permlang
npx permlang init src --workflow
This creates the inventory, a settings file, and a GitHub workflow that checks every pull request. Commit all three. The getting started guide walks through the rest in about ten minutes.
[!NOTE] PermLang is new (v0.x). Feedback, false positives, and missed access are all welcome as issues. See the changelog for what each release changes.
Upgrading from 0.3? The check fails with one error until you update PermLang (
npm install --save-dev permlang@^0.4.0), runnpx permlang lockwith the same paths as your check, review the updated inventory, and commit it (details).
What it can't see (yet)
PermLang is upfront about its blind spots, and reports them instead of hiding them:
- Libraries it doesn't know. It understands many popular ones (Stripe, AI SDKs, Redis, databases, HTTP clients, and more). Unknown libraries are listed in every report, and a change that starts using a new one fails the check until someone reviews it.
- Code that can't be analyzed ahead of time, such as
eval, is reported. At the default strictness it's an error until a developer marks it as reviewed and gives a reason. Every such exception is listed. - A few advanced tricks are documented, with tests, in the reference.
Common questions
Does it work with code from AI coding assistants? Yes. It checks the code, whoever wrote it: GitHub Copilot, Cursor, another AI assistant, or a person. It's built for the pull request nobody reads line by line.
How do I see what my MCP server's tools can do?
Run npx permlang check src. The report lists every tool registered with MCP,
the Vercel AI SDK, the OpenAI SDK, OpenAI Agents, LangChain, LlamaIndex, Genkit,
and the other frameworks it recognizes, what each can reach,
and warns when a model could use one to run commands, write data, send to any
address, or read any file or secret it names.
More on AI tools.
How is it different from CodeQL or Semgrep? Those scanners look for known-bad patterns, such as SQL injection. PermLang tracks what your code can reach and flags anything new, even when it looks perfectly normal, like a call to an unfamiliar server. They work well together: PermLang's own repository runs CodeQL too.
Does it run my code or send it anywhere?
No. It reads your source with the TypeScript compiler and never runs it. Your
code stays on your machine or CI runner: the command-line tool makes no network
calls. The GitHub Action downloads what it needs to build PermLang (from npm),
and sends GitHub only its results, which quote short lines of code, for the
pull-request comment and annotations (and code scanning, if you turn that on).
The workflow init writes passes your installed packages, not your code, from
the job that installs them to the job that checks, as an artifact kept for a day.
Is it free? Yes. PermLang is open source under the Apache 2.0 license.
Learn more
License
Apache License 2.0. See LICENSE and NOTICE. The license covers the code; it grants no rights to the PermLang name or marks (Section 6).
