Plugins

July 8, 2026 ยท View on GitHub

Plugins extend career-ops with integrations that need an API key or talk to an external service โ€” things the zero-keys, local-first core doesn't carry. They are opt-in, sandboxed-by-convention, and additive: with no plugins enabled, the core runs exactly as it always has.

This is not the Claude Code plugin (.claude-plugin/). These plugins extend career-ops itself.

Using plugins

node plugins.mjs list          # what's installed + its trust badge
node plugins.mjs available     # bundled + community plugins we've approved
node plugins.mjs add <name>    # install an approved community plugin
node plugins.mjs enable <id>   # show the capability card (then add --confirm)
node plugins.mjs skill <id>    # print a plugin's how-to (if it ships one)

Two gates must both be satisfied for a plugin to run: it must be enabled (node plugins.mjs enable <id> --confirm, which records your consent) and its keys must be in your .env. node doctor.mjs shows what's missing.

Trust badges

BadgeMeaning
๐Ÿ“ฆ bundledShipped in plugins/, reviewed in-tree, auto-updated with the core.
โœ“ approvedA community plugin we reviewed at an exact pinned commit (in the registry).
โ“ community-unverifiedYou installed it from a repo we haven't reviewed โ€” you're trusting the author.
โš ๏ธ off-registryInstalled commit differs from the approved one.

If a plugin's files change without a version bump, career-ops blocks it and asks you to review + node plugins.mjs trust <id> to re-pin (tamper detection).

Writing a plugin

node plugins.mjs new my-plugin     # scaffolds plugins.local/my-plugin/

A plugin is a directory with a manifest.json (validated before any code is imported), an index.mjs (default-exports your hooks), and optionally a skill.md + _helpers. Hooks: provider / ingest / search / notify / export โ€” there is no auto-submit hook. Producers return Job[]; the engine writes them. Reach the network only through ctx.fetch (your manifest allowedHosts is enforced, with SSRF protection). Keys arrive via ctx.env, non-secret settings via ctx.settings.

See plugins/README.md for the full contract + the honest trust model (plain ESM has no hard sandbox โ€” bundled plugins are code-reviewed; your own are your trust).

Publishing + getting approved

  1. Develop locally, then publish your plugin as its own public GitHub repo named exactly career-ops-plugin-<name> (the template repo gives you the right shape + a release workflow). Minimum files: manifest.json, index.mjs, README.md, LICENSE, plus skill.md + test/smoke.mjs to be listable.
  2. File a Plugin registration issue (becomes your plugin's home/changelog).
  3. Open a registry PR (the ?template=plugin-registry.md template โ€” the template repo's release workflow can open it for you on a release tag) that adds your plugins-registry/<id>.json file, pinned to an exact commit. CI (plugin-registry-validate) checks the naming, manifest, min-files, license, egress, and a static audit before a maintainer reviews. Once merged, users can node plugins.mjs add <name> and your plugin ships to them via the normal update.
  4. Updates = one more registry PR bumping your entry's sha + version (your release workflow opens it from your own fork). Users only ever get the commit we approved.

Broadly-useful, low/zero-key plugins may be shipped bundled in plugins/ (e.g. apify, gmail, notion). Bundled plugins are reference seeds: reviewed in-tree, always present, and a working example to copy โ€” kept minimal and stable on purpose, not a home for ongoing feature work.

Improving a bundled plugin โ†’ publish a maintained successor

We don't take feature PRs against bundled plugins. If you want to extend one (more options, a richer mapping, new behavior), own it properly:

  1. Publish career-ops-plugin-<id> with the same id as the bundled plugin (start from the bundled plugin's code โ€” it's MIT and credits its origins).
  2. In your registry PR, set "supersedesBundled": true on your entry.
  3. Once approved + pinned, anyone who runs node plugins.mjs add career-ops-plugin-<id> installs your version, and the engine gives your maintained successor precedence over the bundled reference of the same id. node plugins.mjs available surfaces the link: "gmail โ€” ๐Ÿ” maintained version: career-ops-plugin-gmail".

This keeps the core lean and puts the integration in your hands, with your name on it โ€” while the bundled seed stays as the always-present fallback, so the feature never breaks even if a successor goes quiet. Precedence is granted ONLY to a registry-approved successor installed at its exact pinned commit โ€” so an unprompted, unreviewed community plugin can never shadow a bundled one.

Trust boundary (plainly). The thing this protects is the supply chain: the registry is a reviewed system file and installs pin an exact commit, so no upstream author can push code over a bundled plugin without a maintainer merging their entry. It does not try to stop you from running your own modified code on your own machine โ€” career-ops is local-first and the source is yours; if you edit plugins.local/ or your plugins.lock, you're choosing to run your own version, exactly as you always could. Removing a successor restores the bundled reference.

Community plugins

Every community plugin in the registry is reviewed and pinned to an exact commit (see Trust badges and Publishing + getting approved).

PluginWhat it doesHooksKeys neededAuthor
career-ops-plugin-tavilyTavily search/extract for job scanning, liveness checks, and company research.searchTAVILY_API_KEY@Schlaflied
career-ops-plugin-google-calendarGoogle Calendar ingest โ€” detect upcoming interview events and surface them in the career-ops pipeline.ingestGOOGLE_CALENDAR_CLIENT_ID, GOOGLE_CALENDAR_CLIENT_SECRET, GOOGLE_CALENDAR_REFRESH_TOKEN@Schlaflied
career-ops-plugin-linkedin-alertsLinkedIn job alert ingest โ€” parse LinkedIn alert emails from your Gmail inbox, normalize tracking links to canonical job URLs, and surface them in the career-ops pipeline.ingestGMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN@Schlaflied
career-ops-plugin-outlook-interviewsOutlook interview ingest โ€” detect interview invitation emails via Microsoft Graph, extract company / role / meeting link, and surface them in the career-ops pipeline.ingestMSGRAPH_CLIENT_ID, MSGRAPH_REFRESH_TOKEN (optional: MSGRAPH_CLIENT_SECRET)@Schlaflied
career-ops-plugin-obsidianObsidian export โ€” mirror the tracker into your vault as frontmatter notes queryable by Dataview/Bases; frontmatter belongs to the machine, the note body belongs to you.exportNone@Schlaflied

To add your own plugin to the registry, follow the Publishing + getting approved flow above.

Not a plugin

  • Centralized infrastructure the project would run (hosted aggregation, shared services, proxies) โ†’ a separate, opt-in service, see Discussion #904.
  • Auto-submitting / blind-applying โ†’ out of the core everywhere. career-ops drafts for you to review and submit; it is a decision-support tool, not a bot.