Documentation
Get started fast. Go deep when you need to.
Expiry is an npm CLI for polyglot codebases. Add @expiry comments in TypeScript, Python, Go, Rust, Java, and more — CI checks whether the reason for the hack is gone and can open a cleanup PR. Watch the product video →
5-minute quick start
Try this in any Node.js repo — React, Next.js, Express, or a plain TypeScript monorepo.
-
Try it in 30 seconds (no repo needed)
npm install -g expiry && expiry demo
Creates a sample
expiry-demo/folder with annotated files and runs an offline scan. -
Install the CLI
npm install -g expiry
-
Tag one piece of temporary code
Put a comment on the line above the hack. The format is: condition, then a short note.
// @expiry date:2026-12-31 | remove holiday banner function showHolidayBanner() { return true; } -
Scan your project
expiry scan -p .
Expiry lists every
@expirycomment and whether it is pending, ready to remove, or overdue. -
Check your health score
expiry stats --offline
-
Add CI (optional)
expiry init
Creates
.expiry.jsonand a GitHub Actions workflow. See CI setup below.
How it works
Traditional TODOs say when to delete code (“after Q3”). Expiry ties deletion to a condition your CI can verify — a closed ticket, an upgraded dependency, a removed env var.
The condition is not met yet. Code stays — nothing to do.
The condition is met. Time to delete the code (Pro can open a draft PR).
A date passed or grace period expired. CI should block merges.
Annotation format
// @expiry <condition> | <human note> | owner:team (optional)
- Condition — machine-readable signal (see conditions)
- Note — what to remove and why (for humans in code review)
- Pipe
|— combine conditions: remove when any is satisfied
Tag temporary code
Single-line (most common)
One comment above the code you plan to delete:
// @expiry issue:ENG-442:closed | remove legacy auth prefix
export function legacyAuth(token: string) {
return token.startsWith("legacy_");
}
Real-world examples
// @expiry issue:ENG-442:closed | drop v1 webhook handler
// @expiry dep:react@>=19.0.0 | remove React 18 shim
// @expiry flag:NEW_UI:enabled | delete old layout branch
// @expiry env:LEGACY_CHECKOUT! | remove old checkout path
Multi-line blocks (advanced)
Wrap an entire module or function group when one condition covers multiple lines:
// @expiry-block issue:ENG-100:closed | remove legacy module
function legacyA() {}
function legacyB() {}
// @expiry-end
Works in //, #, and block comments. Expiry counts lines inside the block for health metrics.
Conditions
Pick the signal that means “this hack can go.” Combine with | — Expiry marks ready when any condition is satisfied.
| Condition | Example | Ready when… |
|---|---|---|
date:YYYY-MM-DD |
date:2026-06-01 |
Date passed (overdue after grace period) |
issue:KEY:closed |
issue:ENG-123:closed |
GitHub issue closed |
issue:KEY:closed:jira |
issue:PROJ-1:closed:jira |
Jira issue closed |
dep:pkg@range |
dep:react@>=19.0.0 |
Installed version satisfies semver range |
env:KEY! |
env:LEGACY_MODE! |
Environment variable is unset |
env:KEY=val |
env:NODE_ENV=production |
Env var equals value |
flag:NAME:enabled |
flag:NEW_UI:enabled |
Flag is true in .env |
flag:NAME:removed |
flag:OLD_UI:removed |
Flag unset or disabled |
file:path |
file:dist/legacy.js |
File exists (e.g. migration applied) |
file-absent:path |
file-absent:legacy.sql |
File no longer exists |
manual |
manual |
Human review only — never auto-ready |
CLI commands
All commands accept -p <path> for the repo root (defaults to current directory).
| Command | What it does |
|---|---|
expiry scan |
List all annotations and their status |
expiry check |
CI gate — exits non-zero on ready/overdue (use in pipelines) |
expiry stats |
Health score 0–100 with overdue/ready/pending counts |
expiry validate |
Syntax-only — no network, catches malformed conditions |
expiry report |
Markdown or SARIF output for security tooling |
expiry init |
Create .expiry.json + GitHub workflow template |
expiry watch -i 30 |
Poll every 30s and print changes (local dev) |
expiry cleanup --dry-run |
Preview auto-removal (Pro license) |
expiry pr --draft |
Open draft cleanup PR (Pro license) |
expiry notify |
Send Slack/email summary (Pro license) |
expiry scan -p . --offline expiry check -p . expiry report -p . --format sarif -o expiry.sarif
CI setup
Quick setup
- Run
expiry initin your repo root - Commit
.expiry.jsonand.github/workflows/expiry.yml - Add
EXPIRY_LICENSE_KEYto GitHub Actions secrets (free tier works without a key for scan/check) - Tag temporary code with
@expirycomments and open a PR — the workflow runs automatically
GitHub Actions workflow
Generated by expiry init, or add manually:
name: Expiry
on:
pull_request:
push:
branches: [main]
jobs:
expiry:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm install -g expiry
- run: expiry check -p .
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EXPIRY_LICENSE_KEY: ${{ secrets.EXPIRY_LICENSE_KEY }}
Action inputs (reference)
| Input | Default | Description |
|---|---|---|
fail-on-overdue | true | Fail CI when date conditions are past grace period |
fail-on-ready | true | Fail when code is ready for removal |
comment-on-pr | true | Post health score comment on pull requests |
auto-cleanup-pr | false | Open draft removal PR (Pro license) |
license-key | — | Pro/Team license from dashboard |
Outputs: health-score, ready, overdue, cleanup-pr-url
Configuration
.expiry.json in your repo root (created by expiry init):
{
"gracePeriodDays": 7,
"issueProvider": "auto",
"github": { "owner": "your-org", "repo": "your-repo" },
"jira": {
"baseUrl": "https://your.atlassian.net",
"email": "you@company.com"
}
}
Environment variables
| Variable | Required for |
|---|---|
GITHUB_TOKEN | issue: checks in CI (usually provided by Actions) |
EXPIRY_LICENSE_KEY | Pro features — auto-cleanup PRs, Slack, SARIF |
JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN | Jira issue conditions |
Pro features
Free tier covers scan, check, validate, and CI gating. Pro and Team add automation after conditions are met:
- Auto-cleanup PRs — draft PR with code removal when ready
- Slack & email alerts — notify owners when debt is overdue
- SARIF export — feed findings into GitHub Advanced Security
- Jira sync — link annotations to tickets
export EXPIRY_LICENSE_KEY="your_key_from_dashboard" expiry cleanup --dry-run -p . expiry pr --draft -p . expiry notify -p .
Troubleshooting
Scan finds nothing
Comments must include @expiry (case-insensitive) and a valid condition token. Run expiry validate -p . to see parse errors. Supported comment styles: //, #, --, /* */, ///, //!, and HTML <!-- -->. Default scan includes JavaScript, TypeScript, Python, Go, Rust, Ruby, Java, C, C++, C#, Kotlin, PHP, Swift, SQL, Vue, and Svelte.
Issue conditions stay pending in CI
Ensure GITHUB_TOKEN has permission to read issues, and the issue key matches (e.g. ENG-442). For Jira, set JIRA_* env vars and use issue:KEY:closed:jira.
expiry check fails but I want to merge
Either remove/update the annotation, fix the underlying condition, or use manual for items that need human sign-off. Overdue date conditions fail after gracePeriodDays (default 7).
Pro commands say license required
Set EXPIRY_LICENSE_KEY from your dashboard. Keys are tied to your Stripe subscription — monthly keys renew every ~35 days, annual keys every ~370 days. In CI, the GitHub Action validates your key online against /api/license/validate so cancelled subscriptions stop working immediately. Update the secret when your dashboard shows a renewed key.