How the Career-Ops Plugin System Isolates Third-Party Integrations from Core System Instructions
The career-ops plugin architecture uses a defense-in-depth strategy combining manifest validation, capability locking, user-driven enablement, and controlled I/O to ensure third-party code cannot alter core workflow or system instructions.
The santifer/career-ops repository implements a rigorous plugin isolation model that protects the integrity of your job-search pipeline. Every third-party integration runs through multiple security layers before executing, with the core system retaining complete authority over data writes and instruction flow. This design prevents malicious or buggy plugins from corrupting your tracker, leaking credentials, or reinterpreting system-level behavior.
Explicit Plugin Manifest and Registry
All plugins must ship a manifest.yml that declares their identity, available hooks, required environment variables, and AI skill status. The core system refuses to load any plugin without a valid manifest.
In plugins/_registry.mjs, the registry validates these declarations and builds a catalogue of known plugins:
- Plugin ID and version constraints
- Hook definitions — the only entry points plugins may expose
- Environment variable requirements
- Successor relationships for plugin chaining
This manifest-first approach ensures the core system never discovers unvetted code.
Capability Locking with User Consent
Before any plugin runs, plugins/_lock.mjs enforces a capability lock stored in plugins.lock.json. This file records:
- Which plugins have been reviewed and approved
- Explicit capabilities granted (network access, file read/write scopes)
- User consent timestamps
If a plugin attempts an action outside its locked capabilities, the runtime aborts immediately with a descriptive error. This prevents privilege escalation attacks where a plugin might try to expand its permissions at runtime.
Config-Driven Enablement
Plugins are disabled by default as defined in config/plugins.example.yml. Users must create their own config/plugins.yml (git-ignored) and explicitly set:
plugins:
notion:
enabled: true
gmail:
enabled: true
Because this configuration file is user-owned and excluded from version control, a malicious plugin cannot silently enable itself. The core system checks this file before loading any plugin code.
Environment Variable Gating
Each plugin declares its required secrets in its manifest. The doctor.mjs helper validates that these environment variables are present before the plugin loads:
# Example: Notion plugin requires NOTION_ACCESS_TOKEN
# If missing, the plugin fails to initialize with a clear diagnostic
This prevents plugins from operating with missing or substituted credentials that could redirect data to attacker-controlled endpoints.
Restricted Hook Surface via CLI
Plugins execute exclusively through the plugins.mjs CLI with validated hook names. The core code at lines 128-148 of plugins.mjs verifies:
// Simplified flow from plugins.mjs
const requestedHook = process.argv[3];
const allowedHooks = manifest.hooks.map(h => h.name);
if (!allowedHooks.includes(requestedHook)) {
throw new Error(`Hook "${requestedHook}" not declared in manifest`);
}
Plugins cannot call arbitrary internal functions or access unexposed core APIs.
Write-Ownership Rules and Data Sanitization
The core system owns all writes to user-facing directories: data/, reports/, and output/. Plugins may read these locations but never invoke write helpers directly.
When a plugin returns job data, plugins.mjs (lines 44-45) sanitizes the output:
// Inside plugins.mjs – after plugin execution
const cleanJob = keepCanonicalFields(pluginResult);
// Only canonical fields survive; plugin-specific keys are stripped
This guarantees that plugins cannot:
- Corrupt existing tracker entries
- Inject malformed rows with extra fields
- Alter file formats or schemas
Separate Execution Path for Provider Plugins
Provider-type plugins (e.g., Gmail scanners) receive additional isolation. Rather than running through plugins.mjs, they execute inside the dedicated scan.mjs pipeline with its own sandboxed environment.
As noted in plugins.mjs (lines 12-15), this separation keeps I/O-heavy integrations away from the core instruction set, reducing attack surface for network-facing code.
Practical Usage Examples
List and Review Available Plugins
node plugins.mjs list
This displays each plugin's capability card derived from its manifest, showing exactly what permissions would be granted.
Enable a Plugin with Explicit Confirmation
node plugins.mjs enable notion --confirm
The --confirm flag requires interactive acceptance of the capability card. Without this flag, the command prints the reviewable permissions but makes no changes.
Execute a Whitelisted Hook
node plugins.mjs run gmail export --dry-run
The export hook must be declared in the Gmail plugin's manifest. The --dry-run flag previews actions without executing them.
Scaffold a New Community Plugin
node plugins.mjs new my-awesome-plugin
This generates a template in plugins.local/ (git-ignored), giving you full source control while the core system treats it as untrusted until explicitly enabled.
Key Isolation Files in Career-Ops
| File | Isolation Function |
|---|---|
plugins/_registry.mjs |
Validates manifests and builds the plugin catalogue |
plugins/_lock.mjs |
Stores and enforces user-approved capability cards |
plugins.mjs |
Validates hooks, sanitizes output, controls execution flow |
config/plugins.example.yml |
Documents the disabled-by-default policy |
scan.mjs |
Provides separate sandboxing for provider-type plugins |
Summary
- Manifest validation ensures only declared, vetted plugins are discoverable
- Capability locking enforces runtime permission boundaries with user consent
- Disabled-by-default configuration prevents silent plugin activation
- Environment gating blocks missing credential scenarios
- Restricted hook surface limits plugins to explicitly exposed functions
- Write-ownership rules keep core data safe from plugin corruption
- Separate provider paths add isolation for network-heavy integrations
Frequently Asked Questions
Can a plugin enable itself without my knowledge?
No. The enablement configuration lives in config/plugins.yml, which is git-ignored and user-owned. A plugin cannot modify this file to activate itself—you must explicitly set plugins.<id>.enabled: true and run the enable command with --confirm.
What happens if a plugin tries to access undeclared capabilities?
The runtime aborts with a clear error. plugins/_lock.mjs compares the plugin's requested action against its locked capabilities; any mismatch triggers immediate termination without affecting core workflow.
How does career-ops prevent plugins from corrupting my job tracker?
All file writes are owned by the core system. Plugins return data through a sanitized channel where keepCanonicalFields() strips non-standard fields. The core then performs the actual write, ensuring schema integrity.
Are provider plugins like Gmail scanners more dangerous than other plugins?
They receive additional isolation through scan.mjs, which runs them in a separate execution environment from the core instruction set. This design acknowledges their higher network exposure and limits potential blast radius.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →