# How the Career-Ops Plugin System Isolates Third-Party Integrations from Core System Instructions

> Discover how the career-ops plugin system isolates third-party integrations from core instructions using manifest validation, capability locking, and controlled I/O. Protect your core workflow.

- Repository: [Santiago Fernández de Valderrama/career-ops](https://github.com/santifer/career-ops)
- Tags: internals
- Published: 2026-08-20

---

**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`](https://github.com/santifer/career-ops/blob/main/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`](https://github.com/santifer/career-ops/blob/main/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`](https://github.com/santifer/career-ops/blob/main/config/plugins.example.yml). Users must create their own [`config/plugins.yml`](https://github.com/santifer/career-ops/blob/main/config/plugins.yml) (git-ignored) and explicitly set:

```yaml
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:

```bash

# 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:

```javascript
// 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:

```javascript
// 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

```bash
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

```bash
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

```bash
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

```bash
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`](https://github.com/santifer/career-ops/blob/main/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`](https://github.com/santifer/career-ops/blob/main/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.