npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wrongstack/plugins

v0.317.2

Published

Official WrongStack collection of focused plugins for code quality, security, observability, planning, and agent coordination

Readme

@wrongstack/plugins

First-party plugin collection for WrongStack. Sixty-four focused, single-purpose plugins ship in this package. Passive, broadly applicable diagnostics and bounded safety checks load automatically; plugins with automatic mutation, policy enforcement, network egress, background work, or provider-call semantic changes are opt-in.

What this is

Each plugin is a self-contained ESM module under src/<name>/ that exports a default Plugin object. The host's plugin loader (@wrongstack/core/plugin/loader) accepts, validates, and setup()s them. Plugins register tools on the host's ToolRegistry and may also register hooks (e.g. secret-scanner registers PreToolUse + PostToolUse hooks).

Official plugins are loaded lazily through factories generated from the typed manifest. The canonical default is each manifest entry's audit.defaultState: list an inactive plugin with { name: '<plugin>', enabled: true } to opt in, or list an active plugin with enabled: false to opt out.

Plugin catalog

Plugin identity, import paths, versions, API ranges, and descriptions are generated from the typed manifest in PLUGIN_CATALOG.md. Run pnpm plugins:manifest:write after changing the manifest.

Removed plugins (use built-in tools instead)

| Removed | Replacement | Why | |---|---|---| | web-search (removed in e03e39d1) | Built-in search + fetch tools in @wrongstack/tools | The built-in tools have native caching, dedup, ranking, DNS-pinned SSRF protection, TurndownService markdown, binary-content rejection, and structured errors. | | json-path (removed in e03e39d1) | Built-in json tool in @wrongstack/tools (action: query | validate | transform | merge) | The built-in json tool already supports JMESPath queries, schema validation, transforms, and deep-merge via a single action parameter. |

If a user lists either name in config.plugins, the loader emits a one-shot log.warn and skips loading. See DEPRECATED_PLUGIN_NAMES in packages/cli/src/wiring/plugins.ts for the canonical list and migration hints.

Per-plugin quick reference

1. auto-doc — JSDoc/TSDoc generation

Tools: auto_doc (mutating)

Generates JSDoc/TSDoc comments and either writes them to the file or returns a preview. Pass dry_run: true to see what would change without writing — the same tool, no separate preview tool.

// Generate doc comments for every export in src/agent.ts, preview only
auto_doc({ files: ["src/agent.ts"], style: "tsdoc", dry_run: true })

2. git-autocommit — AI commit messages

Tools: git_autocommit (mutating, confirm permission)

Stages the listed files (or all changed files when files: []) and creates a commit with a conventional-commit message derived from the diff. Warns when other worktrees are active (likely parallel agents editing the same repo) so the user can verify the diff before commit.

git_autocommit({ type: "fix", scope: "session", message: "..." })

3. shell-check — bash script linting

Tools: shellcheck (mutating — writes the CSV report)

Two modes: pass files: ['scripts/deploy.sh'] to lint specific files or directory: 'scripts', pattern: '*.sh' to recursively scan.

4. cost-tracker — token + USD tracking

Tools: cost_summary, cost_reset, cost_export

Listens to the provider.response event, computes cost from the models.dev-backed api.modelsRegistry (Layer 2 of the lookup chain), with a bundled PRICING table as the baseline (Layer 3) and a pricingOverrides config field as the top-priority escape hatch (Layer 1). The pricingOverrides field is the user-facing tool for correcting a specific model's price without waiting for a plugin release. See the cost-tracker source for the full lookup chain.

// Per-model override (USD per 1M tokens, lowercased model id)
{
  "extensions": {
    "cost-tracker": {
      "pricingOverrides": {
        "gpt-4o": { "input": 7, output: 21 },
        "anthropic-test-model": { "input": 4, output: 20 }
      }
    }
  }
}

5. file-watcher — filesystem events

Tools: watch_start, watch_stop, watch_list

Wires node:fs.watch listeners and stores the handles in module scope. The CLI hooks these events to the per-project mailbox via the dep-watcher bridge so dependency-manifest changes (package.json, go.mod, etc.) trigger tech-stack audits.

6. cron — in-session recurring tasks

Tools: cron_schedule, cron_list, cron_cancel

Schedules timers with api.extensions.register('beforeIteration', ...). All timers are tracked in module-scope state and torn down on plugin unload so a hot-reload cycle doesn't leak setTimeout handles (audited 2026-06-03, see "H1 audit pattern" below).

7. template-engine — file templates

Tools: template_expand, template_render, template_create, template_list

Three template forms: {{var}} substitution, {{#if var}}…{{/if}} conditionals, {{#each items}}…{{/each}} loops. The store is in-memory and module-scoped (audited 2026-06-03).

8. semver-bump — conventional commits → version

Tools: semver_bump, semver_current, semver_changelog

Reads the git log since the last tag, infers the next version (major/minor/patch) from the conventional-commit types, and can tag the new commit. changelog generates a markdown changelog between two refs.

9. secret-scanner — credential blocker + output leak detector

Tools: secret_scanner_status, secret_scanner_test Hooks:

  • PreToolUse with matcher bash|write|edit (configurable via matcher)
  • PostToolUse with matcher * (configurable via postToolUseMatcher)

PreToolUse (prevention — before the tool runs): Mirrors 21 simple patterns from core/src/security/secret-scrubber.ts (LLM provider keys, GitHub PATs v1+v2, AWS, GCP, Slack, Stripe, Twilio, Telegram, JWT, PEM private keys, HuggingFace/Replicate/ Perplexity/Groq, Bearer tokens, mongo/postgres/mysql/redis URIs). Read-only tools (read, fetch) are excluded from PreToolUse by default since secrets flowing IN to them are fine.

PostToolUse (detection — after the tool runs): Scans tool OUTPUT for secrets that leaked through. Since the tool has already run, the hook cannot block — instead it injects additionalContext so the LLM knows not to echo, store, or commit the leaked value.

Three modes (config.extensions['secret-scanner'].mode) for secret-scanner:

  • block (default): returns HookOutcome{ decision: 'block', reason }
  • redact: returns HookOutcome{ decision: 'allow', modifiedInput, additionalContext } with the offending strings replaced by [REDACTED:type]
  • allow: only logs; never blocks
// Basic config
{
  "extensions": {
    "secret-scanner": {
      "mode": "block",
      "matcher": "bash|write|edit",
      "postToolUseMatcher": "*"
    }
  }
}

Custom patterns (customPatterns): Append your own credential patterns alongside the 21 built-in ones. Each entry is a { type, regex, description? }. Invalid regex entries are silently skipped.

{
  "extensions": {
    "secret-scanner": {
      "customPatterns": [
        {
          "type": "internal_api_key",
          "regex": "IAK-[A-F0-9]{40}",
          "description": "Internal API key format"
        },
        {
          "type": "custom_jwt",
          "regex": "eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+"
        },
        {
          "type": "vault_token",
          "regex": "hvs\\.[A-Za-z0-9_-]{90,}"
        }
      ]
    }
  }
}

Custom patterns are detected by all hooks (PreToolUse block/redact, PostToolUse leak detection) and by the secret_scanner_test tool. They are reset to base-only on teardown (H1 pattern).

The high_entropy_env pattern from the output scrubber is intentionally omitted — too slow and too false-positive prone for a synchronous pre-tool gate.

10. todo-tracker — persistent backlog

Tools: todo_tracker_list, todo_tracker_add, todo_tracker_complete, todo_tracker_drop, todo_tracker_remove, todo_tracker_pull, todo_tracker_status

Closes a gap that no existing tool fills: a per-project backlog that survives across sessions. The built-in todo tool mutates ctx.todos (session-scoped, auto-clears when all items complete); PlanFile and TaskFile are also session-scoped. This plugin writes a per-project JSON file with atomic write (temp + rename).

Cross-session bridge: todo_tracker_pull returns active items for the LLM to re-register with the built-in todo tool (which mutates ctx.todos). The plugin never touches ctx.todos directly — that separation respects the existing session/tool boundary.

Storage: per-project JSON at the path provided by paths.projectDir (via the host's wiring) or via the explicit todo-tracker filePath config field.

// Explicit override (use when the host doesn't supply paths.projectDir)
{
  "extensions": {
    "todo-tracker": {
      "filePath": "/abs/path/to/todo-tracker.json"
    }
  }
}

11. token-budget — per-session token enforcement

Tools: token_budget_status Hooks: Stop + PostToolUse (matcher *)

Complements cost-tracker (which tracks cost in USD) by enforcing a hard token budget. When usage crosses warnPercent, a one-shot PostToolUse injection tells the LLM to start wrapping up. When it crosses stopPercent, the Stop hook blocks the agent loop.

{
  "extensions": {
    "token-budget": {
      "limit": 500000,       // hard token limit (prompt + completion)
      "warnPercent": 80,     // inject "wrap up" at this %
      "stopPercent": 100,    // trigger Stop at this %
      "model": ""            // "" = all models; or restrict to one
    }
  }
}

limit: 0 (default) = tracking only (no enforcement). The token_budget_status tool reports the exact consumed/remaining breakdown.

12. lint-gate — pre-write lint enforcement

Tools: lint_gate_status Hooks: PreToolUse (matcher write|edit)

Runs biome (or eslint) on the would-be file content before the write or edit commits it. For write, the full content is linted via a temp file. For edit, the current file is read, the old_string → new_string replacement is applied in-memory, and the result is linted.

{
  "extensions": {
    "lint-gate": {
      "linter": "auto",       // "biome" | "eslint" | "auto"
      "mode": "warn",         // "block" | "warn" | "fix"
      "severity": "error",    // "error" | "warning"
      "timeoutMs": 10000,     // linter process timeout
      "fixRules": []          // when mode=fix, limit auto-fix to these rules only
    }
  }
}

Modes:

  • block: refuses the write/edit; LLM must fix lint errors first
  • warn (default): injects lint errors as context; write proceeds
  • fix: auto-runs biome check --write / eslint --fix, substitutes the fixed content via modifiedInput (write only; edit falls back to warn). Use fixRules to limit which rules are auto-fixed:
// Only auto-fix formatting and import types; leave noExplicitAny as warning
{
  "extensions": {
    "lint-gate": {
      "mode": "fix",
      "fixRules": ["format", "lint/style/useImportType"]
    }
  }
}

13. branch-guard — protected branch enforcement

Tools: branch_guard_status Hooks: PreToolUse (matcher bash|git_autocommit)

Blocks git commit, git push, and git merge on protected branches (default: main, master). Checks the current branch via git branch --show-current. When the working tree is dirty, the block reason includes a safe stash workflow:

git stash → git checkout -b feat/my-change → git stash pop → git commit ...
{
  "extensions": {
    "branch-guard": {
      "branches": ["main", "master", "release/*"],
      "mode": "block",         // "block" | "warn"
      "blockCommit": true,
      "blockPush": true,
      "blockMerge": true
    }
  }
}

Each operation type can be individually toggled. git_autocommit tool calls are treated as commits.

14. diff-summary — post-write/edit diff injection

Tools: diff_summary_status Hooks: PostToolUse (matcher write|edit)

After every write or edit completes, runs git diff -- <path> and injects a capped unified diff into the LLM's context as additionalContext. Gives the LLM immediate visibility into what its change actually did to the file — confirming the edit applied correctly and showing surrounding context.

{
  "extensions": {
    "diff-summary": {
      "maxLines": 50,       // cap diff context at N lines
      "showStat": true,     // include "+N -M" summary line
      "mode": "diff"        // "diff" | "stat" | "off"
    }
  }
}

Modes:

  • diff (default): injects unified diff body (capped at maxLines) + +N -M header
  • stat: injects only +N -M counts (no diff body)
  • off: disabled entirely

For untracked/new files: uses git diff --no-index /dev/null <path>. For non-git repos: silent fallback (no injection). Skips on tool errors.

15. commit-validator — conventional-commit enforcement

Tools: commit_validator_status Hooks: PreToolUse (matcher bash|git_autocommit)

Parses the commit message from git commit -m "..." (bash) or the message field (git_autocommit) and validates it against the conventional-commit format:

<type>[(scope)][!]: <description>

Checks:

  • Valid format (type + colon + subject)
  • Type is in allowedTypes (if configured; empty = allow all standard + custom)
  • Scope is present (if requireScope: true)
  • Subject ≤ maxSubjectLength (default: 72)
  • Subject does not end with a period
{
  "extensions": {
    "commit-validator": {
      "mode": "block",          // "block" | "warn"
      "requireScope": false,    // require (scope) in message
      "allowedTypes": [],       // empty = all; or ["feat", "fix", "docs"]
      "maxSubjectLength": 72
    }
  }
}

Standard types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.

16. format-on-save — automatic biome formatting

Tools: format_on_save_status Hooks: PostToolUse (matcher write|edit)

After every write or edit completes, runs biome format --write <path> on the file on disk. Silently reformats — no blocking, no warnings, just clean code. If the file changed (formatting was applied), injects additionalContext so the LLM knows the file was reformatted.

{
  "extensions": {
    "format-on-save": {
      "enabled": true,       // master switch
      "timeoutMs": 5000      // biome process timeout
    }
  }
}

Biome detection runs once at setup(). If biome is not installed, the hook is a silent no-op. Works alongside lint-gate (which lints BEFORE the write) and diff-summary (which shows the diff AFTER) — together they form a complete write pipeline: lint → write → format → diff.

17. test-runner-gate — automatic test execution

Tools: test_gate_status Hooks: PostToolUse (matcher write|edit)

After every write or edit to a source file, maps the source path to its test file (using configurable patterns), runs vitest run <test-file> --reporter=json, and injects the result. On failure, injects failure details (test name + error, up to 5) so the LLM knows immediately what broke.

{
  "extensions": {
    "test-runner-gate": {
      "enabled": true,
      "command": "npx vitest run",
      "timeoutMs": 30000,
      "testFilePatterns": [
        "tests/{name}.test.ts",
        "src/{name}.test.ts",
        "tests/{name}-exec.test.ts"
      ],
      "injectOnPass": false
    }
  }
}

Patterns use {name} (basename), {path} (path-no-ext), {dir} (dirname). Skips test files themselves, tool errors, and missing test files (silent).

18 — import-organizer

Tools: import_organizer_status Hooks: PostToolUse (matcher write|edit)

After every write or edit, runs biome check --write --unsafe (or eslint --fix if biome is not installed) on the just-saved file. The --unsafe flag enables auto-organizing imports: re-sorts them alphabetically within import groups, merges duplicate imports from the same module, and removes unused ones. Other safe fixes (formatting, style) are applied alongside.

The hook runs after the tool completes (PostToolUse) and reads the file from disk — so write tool's content parameter and edit tool's post-edit disk state are both handled correctly. If the file no longer exists at hook time (e.g. immediately deleted), the hook silently skips.

The hook is idempotent: running biome twice on the same file produces the same result. Lint issues that biome cannot auto-fix are reported in additionalContext so the LLM knows to clean them up manually.

{
  "extensions": {
    "import-organizer": {
      "enabled": true,
      "command": "npx @biomejs/biome check --write --unsafe",
      "fallbackCommand": "npx eslint --fix",
      "timeoutMs": 10000
    }
  }
}

Use the --unsafe flag with care: it enables rules that can change runtime behavior (e.g. useImportType may force import type for type-only imports, which is a no-op at runtime but can break tooling that introspects import statements). If you prefer the safe-only mode, omit --unsafe.

19 — todo-listener

Tools: todo_listener_status Hooks: PostToolUse (matcher todo)

When the built-in todo tool is called, broadcasts a structured status update to the project mailbox so other agents (terminals, WebUIs, shadow agents) can see what this agent is working on in real time.

{
  "extensions": {
    "todo-listener": {
      "enabled": true,
      "subjectPrefix": "todo: ",
      "broadcastOnChange": true,
      "cooldownMs": 5000
    }
  }
}

Payload shape (mailbox body, JSON):

{
  "count": 4,
  "inProgress": { "id": "auth-flow", "content": "implement OAuth callback" },
  "pending": 2,
  "completed": 1,
  "items": [
    { "id": "auth-flow", "status": "in_progress", "content": "..." }
  ]
}

Dedup + rate limiting:

  • broadcastOnChange: true (default) — identical consecutive payloads (FNV-1a hash over id|status|content) are suppressed. Re-issuing the same list doesn't spam the inbox.
  • cooldownMs: 5000 (default) — minimum interval between two consecutive broadcasts. Prevents the agent loop from flooding the mailbox on every iteration.

Host requirements:

  • Requires api.mailbox (added in this commit) on the host's PluginAPI. Minimal hosts (tests, the LSP server, the standalone TUI without a coordinator) don't construct a mailbox — the hook logs a one-shot warning and silently no-ops.
  • Subject is truncated to 200 chars to keep the inbox readable.

20 — session-recap

Tools: session_recap_status Hooks: Stop

When the agent loop ends, the hook posts a one-page session summary to the project mailbox. Other agents (terminals, WebUIs, shadow agents) can read the recap stream to see what the previous session finished — useful for end-of-day handoff and audit.

The plugin accumulates lightweight metrics from the EventBus during the session:

  • provider.response events → tokens per model
  • tool.* events → tool-call counts (top-5 reported)
  • tool.result events → commit count (via git_autocommit success)
  • First/last activity timestamp → wall-clock duration
{
  "extensions": {
    "session-recap": {
      "enabled": true,
      "subjectPrefix": "session recap: ",
      "includeTranscriptTail": 3,
      "maxBodyChars": 8000
    }
  }
}

Recap payload (mailbox body, JSON):

{
  "session": {
    "id": "sess-42",
    "cwd": "/home/user/proj",
    "startedAt": "2026-06-30T10:00:00Z",
    "endedAt": "2026-06-30T10:32:18Z",
    "duration": "32m18s"
  },
  "tokens": {
    "total": { "input": 12345, "output": 6789 },
    "perModel": [
      { "model": "gpt-4o", "input": 8000, "output": 5000, "invocations": 12 }
    ]
  },
  "tools": {
    "totalCalls": 47,
    "uniqueTools": 8,
    "top": [["read", 18], ["bash", 12], ["edit", 9]]
  },
  "commits": 2,
  "transcriptTail": [
    { "type": "user", "ts": "...", "preview": "last user prompt..." }
  ]
}

Transcript tail: by default the recap includes the last 3 events from api.session.transcriptPath (the JSONL session log) for context. Increase includeTranscriptTail for more history.

Host requirements:

  • api.mailbox — same as todo-listener. Without it, the hook logs a one-shot warn and no-ops.
  • api.session.transcriptPath — when missing the transcript tail is empty but the metrics summary still publishes.

21 — spec-linker

Tools: spec_linker_status Hooks: PostToolUse (write|edit) + PreToolUse (write, when autoFix: true)

Two hooks working together:

PostToolUse (always active when the plugin is enabled): scans markdown files for unlinked references to one of the manifest-listed known plugins and surfaces them to the LLM via additionalContext. The plugin does NOT modify the file — it only injects a low-noise context block listing the unlinked references and their canonical paths so the LLM can fix the file in a follow-up edit.

PreToolUse (only when autoFix: true): scans the would-be content of a write call and returns a modifiedInput.content where each unlinked plugin reference is wrapped in a markdown link. The tool executor then writes the fixed content instead of the original. The fix preserves the original casing (Secret-Scanner becomes [Secret-Scanner](./src/secret-scanner)) and leaves markdown-link / inline-code references untouched.

Why write only and not edit: the edit tool's input is { path, old_string, new_string }new_string is a small patch, not the whole file. Auto-fixing edit cleanly would require re-deriving the new old_string after substitution (a hard string-diff problem), so edit stays read-only and the PostToolUse context tells the LLM what to fix.

Detection rules (both hooks):

  • Source matches config.fileGlobs (default: **/*.md, **/*.mdx)
  • The reference matches one of the manifest-listed plugin names (case-insensitive)
  • It is NOT already wrapped in a markdown link [name](...) or inline code `name`
  • It is NOT a hyphenated/dotted continuation (secret-scanner-config.json does not match secret-scanner)
{
  "extensions": {
    "spec-linker": {
      "enabled": true,
      "fileGlobs": ["**/*.md", "**/*.mdx"],
      "maxReferences": 8,
      "autoFix": false   // set true to enable PreToolUse auto-link
    }
  }
}

Injected context (sample, when 2 unlinked references are found):

🔗 spec-linker: 2 unlinked plugin reference(s) in 'docs/feature-matrix.md'.
Consider wrapping them in markdown links to keep the docs navigable:
- `secret-scanner` → `[secret-scanner](./src/secret-scanner)`
- `token-budget` → `[token-budget](./src/token-budget)`

Why read-only by default: the file might be referenced elsewhere or under review. Surfacing a suggestion lets the LLM (or the user) decide whether to fix it, instead of silently rewriting the file on every save. Opt into autoFix once you're confident in the rewrite.

22 — loop-breaker

Tools: loop_breaker_status Hooks: PreToolUse

Detects repeated tool calls before they execute. It tracks exact-repeat streaks and A-B-A-B oscillations, then either blocks the call or injects a warning depending on mode.

{
  "extensions": {
    "loop-breaker": {
      "enabled": true,
      "mode": "block",        // "block" | "warn"
      "repeatThreshold": 3,
      "oscillationThreshold": 2
    }
  }
}

23 — path-guard

Tools: path_guard_status Hooks: PreToolUse

Protects sensitive paths from accidental writes/edits and destructive shell commands. Defaults cover lockfiles, .env, .git, and migration-like paths; use mode: "warn" for advisory-only enforcement. Writer globs are blocked when their unresolved scope overlaps a protected path; narrow the target or add an allow glob to exempt an intentional scope.

{
  "extensions": {
    "path-guard": {
      "enabled": true,
      "mode": "block",
      "protected": [".env*", ".git/**", "**/migrations/**"]
    }
  }
}

24 — context-pins

Tools: pin_add, pin_remove, pin_list Hooks: system prompt contributor

Stores short durable facts and injects them into the system prompt so they survive context compaction. When the host supplies a project directory, pins persist to a per-project JSON file; otherwise the plugin falls back to in-memory state.

pin_add({ label: "api", text: "Use the v2 billing endpoint for invoices." })

25 — checkpoint

Tools: checkpoint_create, checkpoint_list, checkpoint_restore Hooks: PreToolUse

Captures file snapshots before risky write/edit operations and lets the agent restore a snapshot later. Restore never deletes files that did not exist at capture time; it reports them instead.

checkpoint_create({ paths: ["src/service.ts"], label: "before refactor" })

26 — error-lens

Tools: error_lens_history Hooks: PostToolUse

Distills failed command/tool output into compact diagnostics: likely error line, source frames, repeat count, and a capped history for follow-up inspection.

{
  "extensions": {
    "error-lens": { "enabled": true, "maxFrames": 5, "historyLimit": 10 }
  }
}

27 — dep-guard

Tools: dep_guard_status Hooks: PreToolUse

Supervises dependency installs before they run. It can block deny-listed packages, warn on typosquat lookalikes, and flag unpinned versions or risky install sources.

{
  "extensions": {
    "dep-guard": {
      "enabled": true,
      "mode": "block",
      "deny": ["event-stream"],
      "warnUnpinned": true
    }
  }
}

28 — config-validator

Tools: config_validator_status Hooks: PostToolUse

Validates edited config files immediately after write/edit. Supported formats include JSON, JSONC, YAML, and TOML-like files depending on extension.

{
  "extensions": {
    "config-validator": {
      "enabled": true,
      "extensions": [".json", ".jsonc", ".yaml", ".yml", ".toml"]
    }
  }
}

29 — notify-hub

Tools: notify_send, notify_hub_status Hooks: Stop + event subscriptions

Sends compact JSON notifications to a configured webhook for session stops, tool errors, budget threshold events, and manual notify_send calls. Empty webhookUrl means the plugin idles.

{
  "extensions": {
    "notify-hub": {
      "webhookUrl": "https://hooks.example.test/wrongstack",
      "events": ["session.stop", "tool.error"],
      "timeoutMs": 5000
    }
  }
}

30 — changelog-writer

Tools: changelog_add, changelog_preview, changelog_write

Collects pending Keep-a-Changelog entries during a session and writes them under ## [Unreleased]. It can also collect conventional-commit subjects and map them to changelog sections.

changelog_add({ section: "Fixed", text: "Prevent template path traversal." })

31 — injection-shield

Tools: injection_shield_status Hooks: PostToolUse

Scans tool output for prompt-injection patterns such as attempts to override instructions, exfiltrate secrets, or treat retrieved content as higher-priority commands. It warns via additional context; it does not modify tool output.

{
  "extensions": {
    "injection-shield": { "enabled": true, "tools": "*", "maxFindings": 5 }
  }
}

32 — llm-cache

Tools: llm_cache_status, llm_cache_clear Hooks: provider runner wrapper (opt-in)

Caches identical deterministic provider requests and short-circuits the provider call on a hit. It is disabled by default because caching changes provider-call semantics; sampled/high-temperature calls are intentionally not cached.

{
  "extensions": {
    "llm-cache": { "enabled": true, "maxEntries": 256, "ttlMs": 300000 }
  }
}

33 — model-router

Tools: model_router_status Hooks: provider runner wrapper (opt-in)

Routes provider calls to alternate models based on declarative rules such as input size, tool usage, or dry-run mode. Disabled by default because it changes which model serves a turn.

{
  "extensions": {
    "model-router": {
      "enabled": true,
      "dryRun": false,
      "rules": []
    }
  }
}

34 — prompt-firewall

Tools: prompt_firewall_status Hooks: provider runner wrapper (opt-in)

Scans provider requests and, optionally, responses for credential leaks and prompt-risk patterns. Modes are warn, redact, and block; the plugin is disabled by default because redact/block can alter or stop provider calls.

{
  "extensions": {
    "prompt-firewall": { "enabled": true, "mode": "redact", "scanResponse": true }
  }
}

35 — auto-escalate

Tools: auto_escalate_status Hooks: onError extension (opt-in)

Retries transient provider failures with the next model in a configured escalation ladder. Disabled by default because it changes error-recovery behavior and can increase cost.

{
  "extensions": {
    "auto-escalate": {
      "enabled": true,
      "escalation": ["gpt-5-mini", "gpt-5.5"]
    }
  }
}

36 — token-throttle

Tools: token_throttle_status Hooks: provider runner wrapper (opt-in)

Applies a rolling-window token/minute budget to provider calls. When the next request would exceed the configured rate, the wrapper delays the call instead of failing it.

{
  "extensions": {
    "token-throttle": {
      "enabled": true,
      "tokensPerMinute": 120000,
      "maxDelayMs": 30000
    }
  }
}

Configuration patterns

There are two surfaces for plugin configuration:

  1. Loadingconfig.plugins controls which plugins load.
    {
      "plugins": [
        { "name": "auto-doc", "enabled": true },
        {
          "name": "git-autocommit",
          "enabled": true,
          "options": { "conventionalCommits": true }
        }
      ]
    }
  2. Options — set an entry's options field or use config.extensions["<plugin-name>"]; the CLI merges both before setup(). If both are present, config.extensions wins for duplicate keys.
{
  "plugins": [
    { "name": "auto-doc", "enabled": true },
    { "name": "git-autocommit", "enabled": true },
    { "name": "secret-scanner", "enabled": true }
  ],
  "extensions": {
    "git-autocommit": { "conventionalCommits": true },
    "secret-scanner": { "mode": "block", "matcher": "bash|write|edit" }
  }
}

To disable a single built-in without removing its config:

{ "plugins": [{ "name": "secret-scanner", "enabled": false }] }

H1 audit pattern

All built-in plugins that hold module-scope state follow a strict lifecycle to survive hot-reload without leaking resources. This includes file-backed stores, timers, watchers, hook counters, in-memory caches, provider wrappers, and notification state. The pattern was formalized after a 2026-06-03 audit (the "H1 audit") found that several plugins kept their state inside the setup() closure, where the loader's WeakMap<Plugin, PluginAPI> could not reach it during teardown — meaning timers, filesystem handles, and in-memory caches leaked across plugin reloads.

The H1 pattern:

  1. State at module scope, not in setup() closure. Anything the teardown needs to clean up lives in a const state = {…} block next to the Plugin object.
  2. setup() is idempotent. It clears the state first, then re-initializes from config and the host's API. Calling setup() twice (e.g. across a hot-reload) leaves a clean slate.
  3. teardown() releases every resource. Timers are clearTimeout'd, chokidar watchers are close()'d, caches are cleared. The unregister handle returned by api.registerHook is called.
  4. teardown does not delete on-disk state. File-based plugins (e.g. todo-tracker) leave the file in place — the user may return in a moment to read it.
  5. health() reports per-session counters for /diag plugins visibility.

Plugins that follow this pattern expose the same teardown contract to the host, so the loader can clean up uniformly.

For plugin authors

The minimum viable plugin:

import type { Plugin } from '@wrongstack/core';

const plugin: Plugin = {
  name: 'my-plugin',
  version: '0.1.0',
  description: 'One-line summary shown in `wstack plugins list`',
  apiVersion: '^0.1.10',
  capabilities: { tools: true },
  defaultConfig: {},
  configSchema: {
    type: 'object',
    properties: { /* … */ },
  },
  setup(api) {
    api.tools.register({
      name: 'my_tool',
      description: 'What this tool does',
      inputSchema: { type: 'object', properties: { /* … */ } },
      permission: 'auto',
      mutating: false,
      async execute(input) {
        return { ok: true };
      },
    });
    api.log.info('my-plugin loaded', { version: '0.1.0' });
  },
  teardown(api) {
    // If you hold state, clear it here (see H1 pattern).
    api.log.info('my-plugin: teardown complete');
  },
  async health() {
    return { ok: true, message: 'my-plugin: alive' };
  },
};

export default plugin;

Register the subpath export in package.json#exports and the named re-export in src/index.ts. The centralized package builder discovers plugin entry points from the exports map, so no per-plugin build configuration is needed. Wire it into the CLI's BUILTIN_PLUGIN_FACTORIES if it should auto-load.

For plugins that need host-level data not yet exposed in PluginAPI (e.g. paths.projectDir), extend the type in packages/core/src/types/plugin.ts and PluginAPIInit in packages/core/src/plugin/api.ts, then thread it through DefaultPluginAPI and the wiring layer. See how modelsRegistry was added for cost-tracker (commit 9bed619f).

License

MIT — see top-level LICENSE.