Catch JWT misconfigurations statically — before an attacker forges a token.
A single wrong flag turns JSON Web Token auth into an open door: accept the
"none" algorithm and anyone can send an unsigned token with "role":"admin";
call jwt.decode() instead of jwt.verify() and the claims your app trusts are
attacker-controlled. These mistakes ship constantly, they never fail a happy-path
test, and they are exactly what RFC 8725 (JWT Best Current Practices)
and the OWASP JWT Cheat Sheet
warn about.
jotwatch is a zero-config, language-agnostic gate that scans your source and
flags these patterns with a clean CI exit code.
$ jotwatch .
● 2 JWT blocker(s):
src/auth.js:15 JWT verification accepts the "none" algorithm.
↳ The "none" alg means an unsigned token. An attacker can drop the signature
and forge any claim (RFC 8725 §3.1). Pin an explicit allow-list of signing
algorithms and never include "none".
[JW001]
src/auth.js:21 jwt.decode() reads claims without verifying the signature.
↳ In jsonwebtoken, decode() does NOT check the signature — its claims are
attacker-controlled. Use jwt.verify(token, key, {algorithms:[...]}) ...
[JW002]
2 blockers · 1 warning
Exit code 1 on a blocker, so it drops straight into pre-commit or CI.
Most JWT tools are runtime attack kits (jwt_tool, jwt-scanner) that hammer a
live endpoint, or JWT rules buried inside a big SAST platform. jotwatch is the
opposite: a standalone, dependency-free static gate focused on one thing — JWT
safety — that runs on your source in milliseconds with no server, no config, and
no language plugins.
jotwatch reads each source file line by line and matches a small set of
case-insensitive patterns grounded in RFC 8725 / OWASP. It's a heuristic scanner,
not a type checker: no code is executed, no network calls are made, nothing to
configure. It's a single static Go binary.
The one language-aware detail: a bare jwt.decode() is flagged only in
JavaScript/TypeScript, because in Node's jsonwebtoken decode() skips
verification — while in Python's PyJWT jwt.decode() is the verifying call.
| Rule | Severity | What it flags | Basis |
|---|---|---|---|
| JW001 | blocker | The "none" algorithm is accepted ("alg":"none", algorithms:["none"], …) |
RFC 8725 §3.1 |
| JW002 | blocker | Signature verification disabled (verify=false, verify_signature:false, JS jwt.decode()) |
RFC 8725 §2.2 |
| JW003 | warning | Expiry not enforced (verify_exp:false, ignoreExpiration:true) |
RFC 8725 §3.9 |
| JW004 | warning | A signing secret is a hardcoded string literal passed to a JWT call | OWASP JWT Cheat Sheet |
Rules favor low false positives: JW004 fires only when a string literal is passed
directly as the key argument to a jwt.sign/encode/verify call, so a key loaded
from the environment is never flagged.
go install github.com/jay-tank/jotwatch@latestOr build from source: go build -o jotwatch .
jotwatch # scan the current directory
jotwatch ./src # scan a path
jotwatch --strict # treat warnings (JW003/JW004) as failures too
jotwatch --json # machine-readable output- run: go run github.com/jay-tank/jotwatch@latest ./ --strictExit codes: 0 clean · 1 a blocker (or any finding under --strict) · 2
usage error.
jotwatch is a heuristic gate. To silence a line, add a jotwatch:ignore comment
on it (// jotwatch:ignore or # jotwatch:ignore). To skip whole paths, list path
substrings in a .jotwatchignore file (one per line, # for comments).
jotwatch answers a focused question well — "does this code accept unsafe JWTs?"
— across languages, as a fast static gate. It does not prove your verification is
otherwise perfect (right audience, right issuer, key rotation); it catches the
handful of mistakes that most directly let an attacker forge a token.
MIT © Jay Tank