Docs / Extending / Write a plugin
Write your first plugin
The short version. Crusader plugins are JavaScript, hot-reload while you edit, and community plugins are Free. You register hooks on crusader.* for passive request/response/WebSocket analysis, scanner insertion points, session handling, Intruder payloads, editor tabs, context menus, mobile artifacts, runnable extension tools, and findings. The host api.* surface covers HTTP/replay, Beacon/OAST, history, sitemap, identities, mobile endpoints, findings, UI/output, the Crusader CLI, and gated external programs. Every plugin declares capabilities up front; preview that manifest before installing so locked APIs return structured results instead of surprising the user.
01The plugin model
A Crusader extension is a JavaScript file. There's no build step and no SDK to vendor: you write against globals the runtime injects — crusader (the hooks you register) and api (the host surface you call). During development the runtime hot-reloads on save, so you edit, hit save, and re-run a check without reinstalling.
Community plugins are Free. Authoring, installing, and running a plugin needs no license. Most capability keys are Free too. A handful of advanced API capabilities are gated to Hunter Pro — scanner findings, signed identity replay, hosted Beacon, MCP tools, report packaging, active scanning, mobile/Frida, and the JA3 transport — and they're listed plainly in section 05. Free is a real daily driver here, not a teaser.
The contract is built around consent before capability. A plugin declares everything it wants in a capabilities array; Crusader shows you that manifest before anything runs; sensitive permissions always require an explicit yes. Nothing reaches the network, your history, or your identities unless you approved the capability that gates it.
Plugins are real code with real reach. Only install extensions you've read or trust — the same way you'd vet a Burp BApp. The preview step exists so you can see exactly what a plugin asks for before you say yes.
02A minimal scan-check plugin
Here's a complete, runnable-looking plugin: a passive scan check that flags responses still served over cleartext http. It does no probing — it reads exchanges Crusader already captured and raises a finding through crusader.report. Note the capabilities declaration at the top: reading captured exchanges is Free; writing scanner findings is the scanner.findings capability and is Hunter Pro.
// cleartext-check.js — passive scan check; finding writes are scanner.findings
crusader.capabilities = ["history", "scanner.findings"];
crusader.scanCheck({
name: "Cleartext transport",
// run() is called per in-scope exchange; return nothing to stay quiet
run(exchange) {
// exchange exposes the captured request/response
if (exchange.scheme !== "http") return;
crusader.report({
title: "Response served over cleartext HTTP",
severity: "low",
url: exchange.url,
evidence: `${exchange.method} ${exchange.url} → ${exchange.status}`,
remediation: "Redirect to HTTPS and set HSTS.",
});
},
});
That's the whole plugin. Drop it in the Extensions screen as a local file (or install it from the CLI), browse your target through the proxy, and run a passive scan. On Hunter Pro, matching exchanges become Low findings in Scanner → Findings. On Free, the plugin can still install and run the passive read path, but the finding write returns the structured requires_upgrade result described below.
Keep first plugins passive. crusader.scanCheck runs over already-captured history — it never sends traffic on its own. Anything that actively probes a target needs the scanner.active capability (Pro) and is for systems you're authorized to test.
03Registration hooks
You extend Crusader by registering callbacks on the crusader object. Each hook plugs into one part of the workstation. Register only what you need — every hook maps to a capability you'll declare and the user will approve.
| Hook | What it does |
|---|---|
crusader.extension({ id?, name, inputs?, permissions?, run }) | Register a runnable, user-facing tool. It appears in the Extensions tab and, over MCP, as plugin.<id>. |
crusader.onRequest(fn) | Passive callback for every captured HTTP request. |
crusader.onResponse(fn) | Passive callback for every captured HTTP response. |
crusader.onWsFrame(fn) | Passive callback for every decoded WebSocket frame. |
crusader.scanCheck({ name, run | onResponse }) | A passive check that runs over captured responses and raises findings via crusader.report(...). |
crusader.onInsertionPoints(fn) | Declare custom active-scanner fuzz locations. Alias: onInsertionPoint. |
crusader.onSessionRequest(fn) | Session handling before tool requests go out: refresh a token, re-sign, or fix up auth headers. Alias: sessionHandler. |
crusader.payloadGenerator({ id?, name, generate }) | Feed Intruder a custom, named payload source. |
crusader.onPayload(fn) | Transform each Intruder payload before the wire send. Alias: payloadProcessor. |
crusader.editorTab({ title, request?, response?, render }) | Add a custom tab to the request/response detail view. |
crusader.menu.add(label, fn) | Add a right-click context action on captured requests. |
crusader.onMobileArtifact(fn) | Run when an APK or IPA is loaded on the Mobile page. Aliases: onMobileApk, mobile.onArtifact. |
crusader.report(...) | Emit a finding. Full finding writes are gated by scanner.findings (Pro). |
crusader.classify(body, contentType, status) | Use the same impact oracle the scanner and authz tools use. |
crusader.store | Persistent per-plugin key/value memory: get, set, has, delete, keys, clear. Values are strings. |
crusader.log(s) | Write to the Extensions log. |
04The host api.* surface
Where crusader.* registers behavior, api.* is how your plugin reaches into the host — sending requests, reading the project's data, asking about the license, drawing UI, or registering CLI-facing behavior. Each call is gated by a capability; calling one you didn't declare (or one locked on your tier) returns a structured refusal rather than throwing — see section 08.
| Area | Calls |
|---|---|
| Output and lifecycle | api.output(s), api.append(s), api.log(s), api.ui(block), api.status(s), api.progress({...}), api.sleep(ms), api.cancelled(), api.license(opts?). |
| HTTP and replay | api.request(opts), api.requestAs(identityId, opts), api.replay({...}), api.rawRequest({...}), api.transport.request(opts), api.transport.batch(opts), api.transport.capabilities(), api.cli({ args }), api.exec({ file, args }). |
| Recon and bypass | api.dirsearch(opts), api.forbiddenBypass(opts), api.hopByHop(opts), api.beacon(opts). |
| Data and targets | api.history(opts) for metadata queries, api.historyGet(id) for one full exchange, api.targets(opts), api.compare(idA, idB), api.endpointMeta(opts). |
| Identity | api.identities({ url? }), api.identity({ id, url? }), api.useIdentity(id). Identity rows include scope/replay metadata; they do not expose raw secrets. |
| Site Map | api.sitemap(opts), api.addSitemapEndpoint(opts), api.addSitemapEndpoints({ endpoints }). |
| Mobile | api.mobileArtifact(), api.mobile.artifact(), api.mobileArtifacts(), api.mobileEndpoints(), api.addMobileEndpoint(opts), api.addMobileEndpoints(opts). |
| Findings | api.report(opts). On Free, Pro-gated finding writes return { ok:false, code:'requires_upgrade', ... } rather than throwing. |
The read path is usually api.history, api.historyGet, api.sitemap, api.targets, api.endpointMeta, and api.identities: pull what Crusader already captured, reason over it, and report. For active work, prefer scoped api.request or the Pro api.transport.request JA3/browser-fingerprint transport when HTTP probes are enough.
Stick to the injected surface. Do not assume arbitrary Node modules or filesystem reach. If you need a local program, use gated api.exec; if you need Crusader itself, use api.cli. Both are capability-controlled and return structured errors when blocked.
05Capabilities & permissions
Every plugin declares a capabilities array. This is the permission manifest: it's what preview surfaces, what you approve at install, and what the runtime enforces at call time. Declare the minimum — a passive analysis plugin usually needs only history and maybe sitemap.
Most capabilities are Free (sensitive ones still require explicit consent on install). A set of advanced capabilities is gated to Hunter Pro. Both are listed here so you know which side of the line your plugin sits on before you build it.
| Tier | Capability keys |
|---|---|
| Free | request.hook, gui.form, context-menu, cli.verb, hot-reload, *, exec, exec-external, network, history, sitemap, sitemap-write, wordlist-read, mobile.artifact, mobile.discovery, mobile.sitemap-write, raw-replay, transport, beacon (sensitive keys still require explicit approval) |
| Pro | scanner.findings, identity.store, identity.signed-replay, beacon.callbacks, hosted-beacon, ja3-transport, mcp.tools, gui.panel, report.packaging, scanner.active, mobile.frida |
Aliases are normalized too: identity to identity.store, mcp to mcp.tools, ja3 to ja3-transport, scanner to scanner.active, mobile to mobile.frida, findings to scanner.findings, process/subprocess/shell to exec-external, menu/right-click to context-menu, form to gui.form, cli to cli.verb, and all to *.
Sensitive capabilities always require explicit user consent, regardless of tier. The * wildcard does not silently grant review-required permissions such as exec-external, identity replay, callbacks, or unrestricted network reach.
06Preview, then install
Always preview before you install — yours or anyone's. Preview parses the plugin and prints its capability manifest without running it, so you see exactly what it asks for and how it behaves on your license:
# inspect a plugin's capabilities without installing it
crusader plugin preview ./cleartext-check.js
# or straight from a URL
crusader plugin preview https://example.com/some-plugin.js
The manifest shows, per capability, whether it's allowed on your tier and what consent it needs. Look for three fields:
| Field | Meaning |
|---|---|
requires_review | The plugin asks for sensitive permissions — install needs your explicit approval. |
api_access_state | Which parts of the api.* surface are open vs. locked on your current license. |
requires_upgrade | Per-capability flag: this key is Pro-gated and inert until you upgrade — but it does not block install. |
Install it
Once you're satisfied, install it. A plugin that requests review-required capabilities needs --approve-capabilities on the CLI to confirm you've seen the manifest:
# install, approving the sensitive capabilities you saw in preview
crusader plugin install ./cleartext-check.js --approve-capabilities
You can also load a local file directly from the Extensions screen, where the same consent prompt appears in the UI. During development, leave the file installed and just keep editing — hot reload picks up each save so you iterate without reinstalling.
07Identity, exec, and MCP safety
Identity replay is scope-aware. Saved identities are host-scoped and target-scope aware. Ask for identities with api.identities({ url }); each row includes applies, in_scope, and replayable_to_url. Use replayable_to_url === true before calling api.requestAs(identityId, opts) or api.request({ identityId, ... }). Do not paste Cookie, Authorization, or CSRF values into plugin prompts or logs.
External programs have three gates. api.exec({ file, args, timeout_ms?, cwd?, env?, stdin? }) can run local tools such as nmap, ghidra, or frida-server, returning { exit_code, stdout, stderr, timed_out, elapsed_ms, file, args }. It only runs when the plugin declares and receives approval for exec-external, the user has enabled the global Allow external programs setting, and the extension sandbox is not Strict. If any gate blocks it, api.exec returns a structured error instead of throwing. api.cli({ args }) runs Crusader's own CLI and only needs exec.
Beacon payloads come from Crusader. Extensions that need OOB callbacks should declare beacon and call api.beacon({ module, tag }) rather than composing raw wildcard hosts. BYO Interactsh/callback infrastructure works on Free; hosted crusader.sh callback generation is Hunter Pro. Callback metadata and history live in the project database for team sharing.
Runnable extensions have CLI and MCP parity. A crusader.extension(...) tool appears in the Extensions tab and over MCP as plugin.<id>. Agents can discover and invoke installed tools with plugin.list and plugin.invoke; authoring helpers include plugin.scaffold and plugin.create. The CLI mirrors the same workflow with crusader plugin preview, crusader plugin install, crusader beacon payload, and crusader mcp tools.
08How locked APIs behave
This is the part that makes one plugin work on every tier. When a plugin calls a Pro-gated API on Free, the call does not throw. It returns a structured requires_upgrade result — the same shape the MCP server returns for gated tools — so your code can branch on it cleanly:
// a hosted-Beacon call on Free returns a result, not an exception
const r = api.beacon({ module: "ssrf", tag: "probe" });
if (r.ok === false && r.code === "requires_upgrade") {
// r also carries: feature, feature_name, required_tier, message, hint, license
api.output(`${r.feature_name} needs ${r.required_tier} — ${r.hint}`);
return; // degrade gracefully; the plugin stays installed and running
}
// …use r normally when ok
The consequences are worth stating plainly:
- The plugin stays installable and runnable on Free. Gated capabilities are inert, not fatal — the rest of the plugin works.
- You branch, you don't catch. Check
r.ok/r.code === "requires_upgrade"rather than wrapping calls intry/catch. - The result is self-describing. It carries
feature,feature_name,required_tier, a humanmessage, ahint(e.g. runcrusader license status), and alicensesnapshot — enough to tell the user what to do.
Write plugins that degrade gracefully: do the Free work unconditionally, and treat Pro APIs as enhancements that announce themselves when locked. That way the same file ships to everyone and quietly does more on Hunter Pro.
Want a guide that isn't here yet? Email [email protected].