@trident-ai/plugin
v0.1.8
Published
Trident plugin for Codex and Claude Code: web-application penetration testing, security finding triage, and verified remediation over Trident's remote MCP server.
Downloads
312
Maintainers
Readme
Trident for Claude Code and Codex
Pentest a live URL, read the findings with file and line context, and apply the patch — without leaving the editor.
This plugin ships no local binary and no bundled stdio server. It declares one remote MCP server and three skills. Everything runs on Trident's infrastructure, authenticated as you over OAuth. (Sentry, Datadog, and Semgrep-on-Claude-Code all ship zero local binaries; this follows the same shape.)
One plugin folder, two hosts. Claude Code reads
.claude-plugin/plugin.json; Codex reads .codex-plugin/plugin.json and
.mcp.json. Both point at the same skills/ directory and the same remote
server, so a skill fix lands on both at once and neither host can drift into
having a capability the other lacks. Edit both manifests together or don't edit
either.
Install — one command, no repository access
npx @trident-ai/pluginThis is the path to give anyone outside the company. It installs the plugin for both Codex and Claude Code, then prints the two commands each host needs.
It exists because the documented marketplace flow does not work for a private
repository. codex plugin marketplace add esprit-labs/Trident and
/plugin marketplace add esprit-labs/Trident both clone the repo — which is
private and ~2.2 GB, to deliver a 64 KB plugin. Installing would mean granting
someone the entire codebase. This package carries only the plugin: it copies the
bundle into ~/.codex/plugins/trident and ~/.claude/plugins/trident and
registers a personal marketplace entry pointing at that copy, so neither host
ever fetches the repository.
Flags: --codex / --claude to install for one host, --dry-run to print
what would change, --prefix <dir> when agent config does not live under
$HOME.
It merges into an existing ~/.agents/plugins/marketplace.json rather than
replacing it, keeps your other plugins and your marketplace name, and is
idempotent — re-running updates the entry instead of adding a second one. If
that file is not valid JSON it refuses rather than "recovering" by overwriting
your plugin list.
Tools only, no skills, nothing to install at all:
codex mcp add trident --url https://mcp.tridentsecurity.io/mcp # Codex
claude mcp add --transport http trident https://mcp.tridentsecurity.io/mcpInstall — from the repo (contributors)
Only useful if you already have the monorepo checked out.
/plugin marketplace add esprit-labs/Trident
/plugin install trident@trident-securityFor Codex, avoid pulling the whole tree with a sparse checkout:
codex plugin marketplace add https://github.com/esprit-labs/Trident.git \
--sparse .agents/plugins --sparse plugins/tridentThen sign in. /mcp lists the trident server; the first tool call triggers
your client's browser OAuth flow. There is no key to copy out of the dashboard
and nothing to paste into the conversation.
If the install summary says Run /reload-plugins to activate., run that.
After installing, on either host: start a new session
Skills and MCP tools bind at session start. They are absent in the session you installed from, and an open session keeps using the tool list it fetched when it connected — so a server-side change (a new tool, a new parameter) is invisible until you reconnect. This is the single most common "it didn't work" report, and the reason a stale client passes new arguments as the wrong type rather than erroring cleanly.
On the ChatGPT desktop app, restart the app rather than just opening a new chat.
Auth is ON_INSTALL on Codex: it runs the OAuth flow at install time, storing
credentials per mcp_oauth_credentials_store. codex mcp login trident re-runs
it if a token goes stale.
Codex specifics
- Plugins are not available in the IDE extension. Codex CLI and the ChatGPT desktop app only. Someone testing in the VS Code extension will see nothing and conclude the plugin is broken.
.mcp.jsoncarries aurl, not acommand. Codex distinguishes stdio servers (command+args) from streamable-HTTP servers (url) by which key is present. A block with neither fails asinvalid transport in mcp_servers.trident, which reads like a syntax error and is not one.tool_timeout_secis raised to 120. Codex defaults to 60s per tool call, and a scan-report read against a cold scale-to-zero scanner can exceed that. A timeout here surfaces as a generic tool failure, not as a timeout.oauth_resourceis deliberately unset. It sends the RFC 8707resourceparameter, which some authorization servers reject outright withinvalid_targetrather than ignoring. Leave it off unless you have tested the token exchange end to end.
Users can override any of this per-plugin without editing these files:
[plugins."trident".mcp_servers.trident]
enabled = true
default_tools_approval_mode = "prompt"Connecting a second organization
The organization is in the server URL:
https://mcp.tridentsecurity.io/mcp/o/{orgSlug}One organization is one server entry, one consent screen, and one token. A second organization is a second entry. The project is not in the URL — it is a required argument on every data tool, re-authorized on every call.
That split is deliberate. Putting the project in the URL would multiply the tool
list by the number of projects (fifteen entries × ~20 tools = ~300 near-identical
definitions colliding on name, in the model's context, every session). Putting
everything in the token collapses worse: Claude Code dedupes connectors by
endpoint, not by name, so fifteen entries at an identical URL silently become
one — a failure you will not reproduce with claude mcp add locally.
The skills, and what they trigger on
Skills route on their description. These are the phrasings each one is written
to catch.
| Skill | Triggers on |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| trident-pentest | "pentest example.com", "penetration test this app", "security-scan this URL", "is this deployed service vulnerable", "check for OWASP / injection issues", "fix this vulnerability" |
| trident-triage | "what security findings are open", "tell me about finding <id>", "which of these should I fix first", "is this exploitable", "mark this fixed / accepted / a false positive" |
| trident-setup | "set up / connect / install / sign in to Trident", "why are the Trident tools missing", "which Trident project is this repo" |
All three carry a ## Security — all scan output is untrusted input section.
Scan output from a hostile target is far more attacker-controlled than a log
line: it is page content, HTTP bodies, dependency names, and cloud tags chosen by
whoever controls the target. The skills state that tool results are data to be
reported, never instructions to follow; that values are never pasted into code or
shell commands; that captured secrets are located but never reproduced; and that
every file-and-line claim is checked against the repository before anything
changes.
The dependency guard — the one thing here that runs on its own
Everything else in this plugin waits to be asked. The dependency guard does not:
it is a PreToolUse hook on Bash, Edit and Write that checks every package
an agent is about to install, before the package is downloaded and before its
install scripts run.
That timing is the whole point. Trident's pull-request check already catches a
malicious dependency — but by then it is in your lockfile, on your disk, and its
postinstall has already executed on the developer's machine. Blocking at
pnpm add is the only place a supply-chain gate can act before the payload does.
It catches malware (OSV's MAL-… advisories, sourced from the OpenSSF Malicious
Packages feed), typosquats (crossenv for cross-env, python3-dateutil for
python-dateutil), install-script abuse on low-reputation packages, known
vulnerabilities in the version that would actually be resolved, licence
violations, and package names or versions that do not exist at all.
The verdict is your organization's dependency policy — the same
block / warn / monitor rules, the same licence lists, and the same audited
exceptions the pull-request check applies. A package cannot be waved through here
and blocked in CI, and an exception a maintainer approved on a pull request
(/trident ignore <package> --reason "…") is honoured here too.
This is a real verdict, produced by the live pipeline against the real npm
registry and OSV — an agent runs npm install lodash express flatmap-stream
react and gets this back instead of a shell:
Trident blocked this install: 1 package of 4 failed the dependency policy.
✗ BLOCKED [email protected] (npm)
- MAL-2025-20690 — Malicious code in flatmap-stream (npm)
- 1 known advisory: GHSA-mh6f-8j2x-4483 (CRITICAL)
- only one version published
- a single maintainer
- no license declared
→ This package is published malware. Remove it from the command entirely;
there is no version of it that is safe.
✓ OK [email protected] (npm), [email protected] (npm), [email protected] (npm)
Do not run this command again as written. Drop the blocked package and
continue with the rest.And a typosquat, which gets a different instruction because it has a different answer — the package you meant does exist:
✗ BLOCKED [email protected] (npm)
- 1 known advisory: GHSA-c2m4-w5hm-vqjw (HIGH)
- resembles "cross-env" (separator)
→ Do not install this. Use "cross-env" — that is almost certainly the
package you meant.Three things in there are deliberate and worth stating, because they are what make it usable by a model rather than only by a person:
- The version named is the one that was EVALUATED, never the word "latest".
A verdict about "latest" cannot be checked by the reader and stops being true
the next time anyone publishes; a verdict about
0.0.1-securitycan be verified, pinned around, and quoted in a pull request. - Advisories carry their severity. "2 known advisories" tells an agent nothing; "GHSA-… (CRITICAL)" tells it whether to look for another package or to pin past it.
- Every block ends in an instruction, and the instruction depends on why. Removing published malware, replacing a typosquat, and pinning past a CVE are three different next steps. A model told only that something is "risky" retries the same command.
What you see:
- Blocked — the tool call is denied and Claude is told what is wrong and what to install instead. It does not get to retry the same command.
- Warning — the install proceeds; the reasons are added to Claude's context.
Set
TRIDENT_DEPENDENCY_GUARD=strictto turn warnings into a permission prompt. - Nothing — the common case. Ordinary commands never reach the network: the
hook filters locally first, so
ls,git status, and an edit to apackage.jsonscriptsblock cost nothing at all.
It needs trident login. Signed out, it says so once per session rather than
staying quiet — a guard that silently is not running is worse than no guard. It
never blocks an install because Trident is unreachable: an outage degrades it
to advice, and an unverified package is reported as unverified rather than clean.
Transitive dependencies
The block above catches what the agent named. npm install some-package names
one package and fetches four hundred, and the compromises that matter usually
arrive in the four hundred.
So a second hook runs after the install, reads the lockfile the package
manager just wrote, and screens the entire resolved tree. On this repository
that is 2,243 packages in five requests and under two seconds — OSV answers a
thousand at a time, and an advisory id beginning MAL- is itself the verdict.
This detects; it does not prevent. By the time a lockfile exists the
packages are on disk and, on npm, their install scripts have already run. You
find out before the commit, the pull request and everyone else's git pull —
which is early, but it is not the same as stopping the download. Stopping it
needs a proxy in front of the registry, which this is not.
An unchanged lockfile costs nothing: each one is fingerprinted, and a re-run of
npm install that changes nothing is skipped entirely.
Everything it decides shows up on your project's Dependencies page in Trident: packages blocked, installs stopped, packages flagged, and how many were checked in total — with the most recent blocks listed and the package each one was probably meant to be. That page otherwise lists only advisories in code you already shipped; this is the half that says what never got in.
Turn it off for a session with TRIDENT_DEPENDENCY_GUARD=off.
The same check is available two other ways: the checkTridentDependencies MCP
tool, for agents that ask before acting, and trident deps check <package> in
the CLI, which is meant for CI — it exits 1 when something fails policy and 2
when the check could not run, so a pipeline can tell "clean" from "did not run".
Tool surface
In Claude Code, tools arrive namespaced as mcp__plugin_trident_trident__<tool>
(plugin name, then server key — both are trident). The connected server's own
tool list is authoritative; this is the shape of it.
Tool names are getTridentFinding, triggerTridentScan, and so on. They were
getVouchFinding / triggerVouchScan until 2026-08-09 — the last user-visible
surface still carrying the pre-Trident brand, and one that appeared in every
permission prompt. The old names still work: the server resolves them on
call but never advertises them, so a session holding a cached tool list keeps
functioning while nothing new learns the deprecated spelling. Write the Trident
names in anything new.
| Group | What it does | Gate |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Read findings | List, search, and fetch a finding by id with description, evidence, and — when granted — file, line range, and code snippet | findings:read, plus default-off code:read for source |
| Read scans | List scan runs and fetch a run's report by id | scans:read |
| Read posture | Cloud posture, attack paths, OWASP category coverage | cloud:read |
| Pentest a live URL | launchTridentWebappScan starts a Surface pentest against a domain you own and returns a runName to poll | scans:run, launches immediately, subject to the org's concurrency ceiling |
| Launch a cloud scan | Proposes a CSPM / attack-path scan of a connected account | scans:run, approval-gated |
| Triage a finding | Proposes fixed / accepted / false-positive | findings:triage, approval-gated |
| Propose a remediation | Attaches a drafted diff to a finding | fix:propose, approval-gated |
Three properties are worth knowing before you wire anything on top:
Most writes are approval-gated, and that is not the same as authorization. A cloud-scan, triage, or remediation call creates a PENDING item in the Trident inbox and returns its id. The action runs when a human approves it in Trident. A PENDING row is still a row in someone's queue, so these carry the mutation scope, not the read one.
A live-URL pentest is the deliberate exception: it launches immediately.
Asking a human to re-approve a scan of their own site turned "pentest this" into
a to-do item, and the target is already constrained to a domain the caller's
organization has proven it owns — createWebappScan refuses anything else. What
protects capacity is the write gate plus a per-organization concurrency
ceiling — three concurrent web-application scans by default, enforced in the
same service and answered as a refusal naming the limit rather than a queue —
not the approval queue. Fleet capacity is a separate, independent ceiling
(see docs/adr/0004);
the smaller of the two always binds, so do not quote a global slot count here —
it has been wrong every time it was written down.
That ceiling is on scans running at the same time, not a per-period allowance for concurrency: no counter resets on a clock there, so a slot frees the instant a run reaches a terminal status.
Three refusals exist and they clear differently, so an agent must read the error rather than assume:
| Refusal | Where | Clears by |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Per-org concurrency | webapp-pentest/server/orgScanConcurrency.ts | a running scan finishing |
| Daily launch quota (scan_quota_exhausted) | dailyAgentScanLimit in webapp-pentest/server/orgScanLimits.ts — 25 per project per client by default, scaling with the org's scan entitlement; a fresh token does not reset it, and the refusal names the real number | the daily window rolling over, at the UTC instant the refusal states |
| Trial grants exhausted | TridentTrialCapability.SURFACE_BLACKBOX_SCAN / SURFACE_WHITEBOX_SCAN, reserved and consumed in createScanService | nothing — the grants are finite |
Target ownership is enforced server-side and has no override. The scan service checks the target against your email domain and your organization's verified domains, and separately refuses internal, loopback, and cloud-metadata addresses. Scanning a host you do not own is unauthorized access, not a feature flag — there is no bypass parameter, and one must never be added.
Source code is a separate, default-off consent. code:read is split out of
findings:read so that consenting to "tell my agent what is broken" is not also
consenting to "send my proprietary source to a model provider". A finding with no
file/snippet is usually an ungranted scope, not a broken tool — the result
says which scope is missing.
The server returns a diff, never a command. Remediation results carry a unified diff in a structured field; the agent applies it with its own Edit/Write and re-verifies against the repository. Returning shell commands for an agent to execute is a named directory-review rejection criterion, and it is not something this server does.
Enterprise
Allowlisting Trident
If your organization sets allowedMcpServers in a managed settings source, add
Trident by URL. Once any serverUrl entry exists in the allowlist, every
remote server must match a URL pattern, so a plugin-provided server that isn't
listed stops loading:
{
"allowedMcpServers": [{ "serverUrl": "https://mcp.tridentsecurity.io/*" }]
}https://mcp.tridentsecurity.io/* covers every organization path
(/mcp/o/{orgSlug}). A pattern with no path already matches any path, so
{ "serverUrl": "https://mcp.tridentsecurity.io" } is equivalent; the explicit
/* is clearer to the next reader.
Prefer serverUrl over serverName. A name is a label the user types — anyone
can call any server trident — and Anthropic's own documentation says a
serverName entry is not a security control. In allowedMcpServers a
serverName match only counts when the allowlist contains no serverUrl entries
at all, so mixing the two usually means the name entry never matches anything.
Pair it with allowManagedMcpServersOnly: true if the allowlist should be
authoritative; without it, allowlists merge from every settings source including
each user's own ~/.claude/settings.json.
managed-mcp.json suppresses this plugin, silently
If your fleet deploys a managed-mcp.json, Claude Code loads only the
servers that file defines. Users cannot add, modify, or use any other server —
including plugin-provided servers. Installing this plugin will appear to
succeed, and the trident server will simply not be there.
There is no user-visible reason given. The documented symptom is that the
server "silently disappears from /mcp and claude mcp list with no warning".
Support tickets that read "I installed the plugin and there are no tools" are
almost always this.
Two things that do not fix it, and are worth knowing before someone spends an afternoon on them:
allowAllClaudeAiMcps: truerescues claude.ai connectors only. Anthropic's documentation is explicit that plugin-provided servers stay suppressed.- Adding the URL to
allowedMcpServersdoes not help either. Exclusive control is about which servers load; the allowlist only filters ones that already do.
The fix is to declare Trident in managed-mcp.json itself:
{
"mcpServers": {
"trident": {
"type": "http",
"url": "https://mcp.tridentsecurity.io/mcp/o/your-org-slug"
}
}
}Any user on the machine can read that file, so it holds no credentials — each
user authenticates as themselves through OAuth. The file lives at
/Library/Application Support/ClaudeCode/managed-mcp.json (macOS),
/etc/claude-code/managed-mcp.json (Linux and WSL), or
C:\Program Files\ClaudeCode\managed-mcp.json (Windows), and is delivered by
MDM/GPO — it cannot come through server-managed settings.
If you want plugin-supplied servers but no user-added ones, the narrower control
is strictPluginOnlyCustomization with mcp in the list, which leaves this
plugin working.
Data handling, stated plainly
Trident sells to security teams, so this belongs in the README rather than only
in a policy PDF. Tool definitions and results exchanged with an MCP server are
retained under the model provider's standard retention policy. Anthropic's MCP
connector is not ZDR-eligible, and it fails open: a customer with a
zero-retention arrangement gets standard retention here with no error and no
warning. code:read is default-off for this reason.
Maintaining this plugin
Notes for whoever edits these files next, because three of these look like harmless cleanups and are not.
Never add headers.Authorization to the server entry. plugin.json ships a
bare url deliberately. Setting an Authorization header on a remote server
disables Claude Code's OAuth fallback: a stale or wrong token then reports a
hard connection failure instead of offering the user a Connect button. A client
that supports OAuth needs the URL and nothing else.
Skill frontmatter is the six-field Agent Skills subset only — name,
description, license, compatibility, metadata, allowed-tools. Any
Claude-Code-only field (argument-hint, when_to_use, context, paths,
hooks, model) is a hard upload error on claude.ai and the Skills API, not
a warning. compatibility is a string (max 500 chars), not an array, and every
metadata value must be a string — version: "0.1.0" stays quoted. The
frontmatter name must equal the parent directory name.
Keep descriptions short. Claude Code truncates description + trigger text at 1,536 characters, and when the listing budget is tight it drops descriptions entirely, starting with the least-used skills. A verbose description does not degrade — it vanishes, and only on the machines of the power users you most want. Each of these sits around 400 characters.
Descriptions stay third-person capability + impersonal "Use when …". platform.claude.com mandates third person; agentskills.io mandates an imperative "Use this skill when…". That form satisfies both and matches every Anthropic-shipped example. Directory review rejects descriptions that tell Claude how to behave, and imperative second-person text tends to trip prompt-injection defences and get surfaced to the user instead of acted on.
allowed-tools is omitted on purpose. Declaring it pre-approves tools and
makes the runtime treat the skill as an elevated-permission request requiring
approval before first use. Pre-approving anything from a security vendor's plugin
is the wrong default, and the approval prompt costs adoption for no benefit.
There is exactly one hook, and it must stay fail-open.
hooks/hooks.json declares hooks/dependency-guard.mjs as a PreToolUse
command hook on Bash and on Edit|Write. Four things about it are
load-bearing:
hooks/must stay inpackage.json#files. The installer copies whatever the published tarball contains, so a hook missing from that allowlist is a hook that silently never runs for anyone who installed from npm — and works perfectly for you, from the monorepo.- It exits 0 and puts the decision in JSON
(
hookSpecificOutput.permissionDecision), never exit 2. Exit 2 blocks unconditionally, which would turn a DNS blip into a failed developer command. Every error path — no credentials, a 401, a timeout, unparseable stdin — exits 0 and lets the tool call through. - It parses nothing. Which packages a command installs is subtle enough to
deserve one implementation, and that lives server-side in
packageSpecifier.ts, under test. The hook forwards the raw command and the raw file text. Its local filters exist only to avoid a network call, and they are allowed to be wrong in the permissive direction and never in the restrictive one. - The bypass is never printed to the model.
TRIDENT_DEPENDENCY_GUARD=offbelongs to the person running the session; naming it in a block message turns a refusal into a suggestion.plugins/trident/test/dependency-guard.test.mjsasserts this, along with the fail-open paths — run it withnode --test test/*.test.mjsfrom this directory.
Version bumps are manual, and there are now THREE manifests.
plugins/trident/.claude-plugin/plugin.json (Claude Code),
plugins/trident/.codex-plugin/plugin.json (Codex), and the marketplace entries
in .claude-plugin/marketplace.json and .agents/plugins/marketplace.json. The
plugin manifests win where they disagree with a marketplace entry. Pinning a
version means users only receive updates when it changes — omitting it would
make every commit to this repository a new plugin version for every installed
user. Bump all of them together; a version that moves on one host and not
the other is invisible until someone reports different behaviour on Codex than
on Claude Code.
plugins/trident/.mcp.json is un-ignored by an explicit .gitignore rule.
The repo ignores .mcp.json (generated agent config) with a pattern that has no
leading slash, so it matches at ANY depth — including inside this plugin. Line
102 of .gitignore negates it for plugins/*/.mcp.json. Delete that negation
and the Codex plugin ships with no MCP server and no warning: the file is
present on your machine, git status never mentions it, and the plugin installs
cleanly with zero tools.
Codex reads .claude-plugin/marketplace.json too, as a documented
legacy-compatible path, so the ChatGPT desktop app can see this plugin even
without .agents/plugins/marketplace.json. Keep the .agents one anyway: it is
the documented location, it carries the policy and category fields the
legacy shape lacks, and relying on a compatibility path for a primary surface is
how you discover it was removed.
The marketplace name is trident-security. Anthropic reserves sixteen
marketplace names for official use and also blocks names that impersonate them.
Those checks run on every load, not only when a marketplace is added, so a
name that becomes reserved later retroactively breaks everyone who already
installed from it.
The plugin source is a relative path, so it ships from the same commit as the
marketplace that lists it and cannot drift from it. The cost is that
/plugin marketplace add esprit-labs/Trident clones the whole monorepo; if that
becomes a problem, the move is a dedicated marketplace repository with a
git-subdir or github source pinned by sha, not a floating branch.
