██╗██╗ ██╗████████╗ ██╗ █████╗ ██████╗
██║██║ ██║╚══██╔══╝ ██║ ██╔══██╗██╔══██╗
██║██║ █╗ ██║ ██║█████╗██║ ███████║██████╔╝
██ ██║██║███╗██║ ██║╚════╝██║ ██╔══██║██╔══██╗
╚█████╔╝╚███╔███╔╝ ██║ ███████╗██║ ██║██████╔╝
╚════╝ ╚══╝╚══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═════╝
────────────────────────────────────────────────────────
v0.1.0 · JWT toolkit for developers & AI agents
jwt-lab
The JWT Swiss-Army Knife for Developers & AI Agents
Encode · Decode · Verify · Inspect · Explain · Keygen · MCP Server
A fast, secure, beautiful, and AI-agent-ready command-line tool for working with JSON Web Tokens (JWTs), plus a full Model Context Protocol (MCP) HTTP/JSON server.
Installation · Quick Start · Commands · MCP Server · Configuration · API Reference
Why jwt-lab?
| Feature | jwt-lab | jwt.io | Other CLIs |
|---|---|---|---|
| 🔐 Security linting & audit | ✅ 6 built-in rules | ❌ | ❌ |
| 🤖 AI-native MCP server | ✅ Full HTTP/JSON API | ❌ | ❌ |
| 🗣️ Natural language encoding | ✅ "admin token expires in 1h" | ❌ | ❌ |
⏰ Time travel (--fake-time) | ✅ Deterministic testing | ❌ | ❌ |
📋 Config as code (.jwt-cli.toml) | ✅ Profiles, defaults, keys | ❌ | ❌ |
| 🎨 Premium terminal UX | ✅ Colors, boxes, tables | ❌ | Partial |
| 🔑 Key generation (RSA/EC/Ed25519) | ✅ JWK + PEM output | ❌ | Partial |
| 📦 Dual ESM + CJS output | ✅ | N/A | ❌ |
🧪 Strict TypeScript, zero any | ✅ | N/A | ❌ |
Installation
# Global install (recommended)
npm install -g jwt-lab
# Or use with npx
npx jwt-lab --help
# Or add to a project
npm install jwt-lab --save-dev
Quick Start
# Encode a JWT with HMAC secret
jwt encode '{"sub":"user1","role":"admin"}' --secret my-secret --exp 1h
# Decode without verification
jwt decode eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSJ9.xxx
# Verify signature + claims
jwt verify <token> --secret my-secret
# Security audit (no keys needed)
jwt explain <token>
# Inspect with full breakdown
jwt inspect <token> --secret my-secret
# Generate key pairs
jwt keygen ec --pem --out-dir ./keys
# Natural language encoding
jwt encode "admin token for user ali@example.com expires in 12h" --secret s
# Start MCP server for AI agents
jwt mcp serve --port 3000
Commands
jwt encode
Encode a JWT from JSON or natural language.
# JSON payload
jwt encode '{"sub":"user1","role":"admin","email":"user@example.com"}' \
--secret my-secret \
--exp 1h \
--iss https://auth.myapp.com
# Natural language (no LLM — deterministic regex parser)
jwt encode "admin user user@example.com expires in 30m" --secret s
# With asymmetric key
jwt encode '{"sub":"svc"}' --key ./private.pem --alg ES256
# With profile from config
jwt encode '{"sub":"user1"}' --secret s --profile access_token
# Copy to clipboard
jwt encode '{"sub":"user1"}' --secret s --exp 1h --copy
# JSON output
jwt encode '{"sub":"user1"}' --secret s --json
Options:
| Flag | Description |
|---|---|
--secret <string> | HMAC secret (HS256/384/512) |
--key <path> | PEM or JWK private key file |
--alg <algorithm> | Signing algorithm |
--exp <duration> | Expiration (e.g., 1h, 30m, 7d) |
--iss <string> | Issuer claim |
--sub <string> | Subject claim |
--aud <string> | Audience claim |
--kid <string> | Key ID in header |
--jti | Generate random UUID as JTI |
--header <json> | Additional header fields |
--profile <name> | Use named profile from config |
--copy | Copy token to clipboard |
--json | Output as JSON |
jwt decode
Decode a JWT without verification.
jwt decode <token>
# From stdin
echo "<token>" | jwt decode -
# Batch mode
cat tokens.txt | jwt decode - --batch
# JSON output
jwt decode <token> --json
jwt verify
Full signature verification and claims validation.
# HMAC
jwt verify <token> --secret my-secret
# Asymmetric key
jwt verify <token> --key ./public.pem --alg ES256
# JWKS endpoint
jwt verify <token> --jwks https://auth.example.com/.well-known/jwks.json
# Required claims
jwt verify <token> --secret s --require sub,iss,exp
# Clock skew tolerance
jwt verify <token> --secret s --leeway 30
# Time travel for testing
jwt verify <token> --secret s --fake-time 2024-01-01T00:00:00Z
Output:
✅ Valid JWT
Algorithm: HS256
Subject: user1
jwt inspect
High-level token breakdown with status, metadata, and security posture.
jwt inspect <token>
jwt inspect <token> --secret my-secret # with verification
jwt inspect <token> --json # machine-readable
jwt inspect <token> --table # table format
Output:
╭───── Token Inspection ──────╮
│ │
│ Status: ✅ valid │
│ Algorithm: HS256 │
│ Subject: user1 │
│ Issuer: auth.example.com │
│ Expires in: 59m 30s │
│ │
│ Lint Findings: │
│ ⚠️ [pii-claims] Payload ... │
│ │
╰──────────────────────────────╯
jwt explain
Static security audit — no keys required.
jwt explain <token>
jwt explain <token> --json # for CI pipelines
jwt explain <token> --table # table format
Output:
🔍 JWT Security Audit
❌ [none-algorithm] Token uses the "none" algorithm
→ Replace "none" with a secure algorithm such as RS256 or ES256
⚠️ [pii-claims] Payload contains claims that may hold PII: email
→ Avoid embedding PII directly in JWT payloads
ℹ️ [hmac-preferred-asymmetric] Token uses HMAC algorithm (HS256)
→ Consider using an asymmetric algorithm such as RS256 or ES256
Built-in security rules:
| Rule ID | Severity | What it checks |
|---|---|---|
none-algorithm | 🔴 error | Algorithm is "none" |
missing-exp | 🟡 warn | Token has no expiration |
long-lived-token | 🟡 warn | Lifetime > 24 hours |
pii-claims | 🟡 warn | Claims containing PII patterns |
missing-nbf-long-lived | 🔵 info | Long-lived token without nbf |
hmac-preferred-asymmetric | 🔵 info | HMAC where asymmetric is preferred |
jwt keygen
Generate cryptographic key pairs.
# EC key pair (default P-256)
jwt keygen ec
# RSA key pair
jwt keygen rsa --bits 4096
# Ed25519
jwt keygen ed25519
# PEM output to files
jwt keygen ec --pem --out-dir ./keys
# JWK with key ID
jwt keygen rsa --jwk --kid my-production-key
MCP Server
jwt-lab includes a full Model Context Protocol HTTP/JSON server for AI agents and programmatic access.
Start the server
jwt mcp serve --port 3000 --host 0.0.0.0
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /encode | Encode a JWT |
POST | /decode | Decode a JWT |
POST | /verify | Verify a JWT |
POST | /inspect | Inspect a JWT |
POST | /keygen | Generate key pair |
POST | /explain | Security audit |
GET | /docs | OpenAPI 3.1 spec |
GET | /health | Health check |
Examples with curl
# Encode
curl -X POST http://localhost:3000/encode \
-H "Content-Type: application/json" \
-d '{"payload":{"sub":"user1"},"secret":"my-secret","alg":"HS256","exp":"1h"}'
# Decode
curl -X POST http://localhost:3000/decode \
-H "Content-Type: application/json" \
-d '{"token":"eyJhbGciOiJIUzI1NiJ9..."}'
# Verify
curl -X POST http://localhost:3000/verify \
-H "Content-Type: application/json" \
-d '{"token":"eyJ...","secret":"my-secret"}'
# Explain (security audit)
curl -X POST http://localhost:3000/explain \
-H "Content-Type: application/json" \
-d '{"token":"eyJ..."}'
# Generate key pair
curl -X POST http://localhost:3000/keygen \
-H "Content-Type: application/json" \
-d '{"type":"ec","format":"jwk"}'
# OpenAPI docs
curl http://localhost:3000/docs
Authentication
Set the MCP_API_KEY environment variable to enable Bearer token authentication:
MCP_API_KEY=your-secret-key jwt mcp serve
# Then include the key in requests:
curl -X POST http://localhost:3000/encode \
-H "Authorization: Bearer your-secret-key" \
-H "Content-Type: application/json" \
-d '{"payload":{"sub":"user1"},"secret":"s","alg":"HS256"}'
Security Features
- Token redaction: Full tokens are never logged; truncated to 20 chars
- Claim redaction: Configure
mcp.redactClaimsto hide sensitive claims in responses - Rate limiting: Sliding window per IP (configurable)
- CORS: Configurable allowed origins
- Input validation: All requests validated with Zod schemas
Configuration
Create a .jwt-cli.toml in your project root:
[defaults]
iss = "https://auth.myapp.com/"
aud = "myapp-api"
alg = "ES256"
[profiles.access_token]
ttl = "15m"
scopes = ["read", "write"]
[profiles.service_token]
ttl = "1h"
aud = "internal-service"
[lint]
piiClaimPatterns = ["email", "phone", "ssn"]
[lint.severityOverrides]
"missing-exp" = "error"
[mcp]
port = 3000
redactClaims = ["email", "phone"]
[mcp.rateLimit]
windowSeconds = 60
maxRequests = 100
The CLI auto-discovers .jwt-cli.toml by walking upward from the current directory. Use --config <path> to specify a custom path.
Priority: CLI flags > config file > built-in defaults
Global Flags
| Flag | Description |
|---|---|
--help | Show help |
--version | Show version |
--fake-time <iso8601> | Override system clock |
--config <path> | Path to config file |
--json | Machine-readable JSON output |
API Reference
Core Library
jwt-lab's core is a pure, I/O-free TypeScript library. All functions return Result<T, E> types — no exceptions thrown.
import { encodeToken, decodeToken, verifyToken, lintToken, generateKeyPair, parseDuration } from 'jwt-lab';
See src/core/ for full API documentation with TSDoc comments.
Tech Stack
| Category | Choice |
|---|---|
| Language | TypeScript 6 (strict mode, zero any) |
| Runtime | Node.js ≥ 22 |
| JWT | jose v6 |
| CLI | commander v14 |
| Validation | zod v4 |
| HTTP | hono + @hono/node-server |
| Build | tsup (dual ESM + CJS) |
| Tests | vitest v4 |
| Terminal | picocolors, boxen, ora, cli-table3 |
Shell Completions
jwt-lab ships with built-in tab-completion scripts for Bash, Zsh, and Fish. The completions are aware of every subcommand and flag — pressing Tab surfaces commands, options, algorithm names, and file paths in context.
How it works
The jwt completions <shell> command prints a shell-specific completion script to stdout. You either eval it at shell startup or write it to a file that your shell auto-loads. No third-party tools are required.
jwt completions bash → prints a Bash completion function + `complete -F` binding
jwt completions zsh → prints a Zsh `_jwt` compdef function
jwt completions fish → prints Fish `complete` directives
Bash
One-liner (current session only):
eval "$(jwt completions bash)"
Persistent — add to ~/.bashrc:
echo 'eval "$(jwt completions bash)"' >> ~/.bashrc
source ~/.bashrc
Or save to the system completions directory (recommended for shared machines):
jwt completions bash | sudo tee /etc/bash_completion.d/jwt > /dev/null
Requires
bash-completionpackage. Install withbrew install bash-completionon macOS orapt install bash-completionon Debian/Ubuntu.
Zsh
One-liner (current session only):
eval "$(jwt completions zsh)"
Persistent — add to ~/.zshrc:
echo 'eval "$(jwt completions zsh)"' >> ~/.zshrc
source ~/.zshrc
Or save to a $fpath directory (the clean approach):
# Pick any directory already in your fpath, or create one
mkdir -p ~/.zsh/completions
jwt completions zsh > ~/.zsh/completions/_jwt
# Make sure the directory is in fpath — add to ~/.zshrc if not already there:
echo 'fpath=(~/.zsh/completions $fpath)' >> ~/.zshrc
echo 'autoload -Uz compinit && compinit' >> ~/.zshrc
source ~/.zshrc
oh-my-zsh users: Save to
~/.oh-my-zsh/completions/_jwt— it's already infpath.
Fish
Fish completions are discovered automatically from ~/.config/fish/completions/. Just save the script there:
jwt completions fish > ~/.config/fish/completions/jwt.fish
Completions take effect immediately — no source or restart needed.
What gets completed
| Context | Completions offered |
|---|---|
jwt <Tab> | All subcommands with descriptions |
jwt encode <Tab> | --secret, --key, --alg, --exp, --iss, --json, … |
jwt verify <Tab> | --secret, --key, --jwks, --oidc-discovery, --alg, --require, --leeway, … |
jwt keygen <Tab> | Algorithm types: RS256 RS384 RS512 ES256 ES384 ES512 EdDSA PS256 PS384 PS512 |
jwt --alg <Tab> | Full algorithm list |
--key <Tab> | File path completion (all shells) |
--config <Tab> | File path completion (all shells) |
jwt completions <Tab> | bash zsh fish |
CI/CD & Publishing
Every push and pull request to main runs the full pipeline:
| Job | Steps |
|---|---|
| Test & Build | lint → type-check → tests → build → CLI smoke test (Node 22 & 24) |
| Security Audit | npm audit at moderate severity |
| CodeQL | Static analysis for JavaScript/TypeScript (separate scheduled workflow) |
| Publish | Runs only on v* tag push → bumps version → builds → publishes to npm with provenance |
Publishing a release
# 1. Bump version
npm version 1.0.0 --no-git-tag-version
git add package.json && git commit -m "chore: bump version to 1.0.0"
git push origin main
# 2. Create a GPG-signed annotated tag
git tag -s v1.0.0 -m "Release v1.0.0"
git tag -v v1.0.0 # verify signature
# 3. Push tag — triggers the publish workflow
git push origin v1.0.0
# 4. Create a signed GitHub Release
gh release create v1.0.0 --title "v1.0.0" --notes "Release notes here" --verify-tag
For a prerelease (e.g. v1.1.0-beta.1), the package is published with the beta dist-tag automatically.
The workflow uses
npm publish --provenance, which attaches a cryptographic SLSA Level 2 attestation proving the package was built from this exact commit.
Required repository secret
| Secret | Description |
|---|---|
NPM_TOKEN | npm Automation token with publish access — add at Settings → Secrets → Actions |
# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test
# Type check
npm run type-check
# Lint
npm run lint
# Start MCP server (dev)
npm run dev:mcp
CI/CD & Publishing
Every push and pull request to main runs the full pipeline:
| Job | Steps |
|---|---|
| Test & Build | lint → type-check → tests → build → CLI smoke test (Node 22 & 24) |
| Security Audit | npm audit at moderate severity |
| CodeQL | Static analysis for JavaScript/TypeScript (separate scheduled workflow) |
| Publish | Runs only on GitHub Release publish → bumps version → builds → publishes to npm |
Publishing a release
- Create and push a tag:
git tag v1.0.0 && git push origin v1.0.0 - Create a GitHub Release from that tag (set it to Published, not Draft)
- The pipeline auto-bumps
package.json, builds, and publishes to npm
For a prerelease (e.g. v1.1.0-beta.1), the package is published with the beta dist-tag automatically.
Required repository secret
| Secret | Description |
|---|---|
NPM_TOKEN | npm automation token with publish access — add in Settings → Secrets → Actions |
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT © jwt-lab contributors
Built with ❤️ for developers and AI agents