Skip to content

Security: AgentPlugged/agentplugged

SECURITY.md

Security policy

AgentPlugged runs on your machine, inside your Git repository. This page says how to report a vulnerability, and what the code protects, against whom, and what it does not. Every mechanism below is named after the module that implements it, so you can check it yourself.

Reporting a vulnerability

Please do not open a public issue, discussion or pull request for a security problem.

Report it privately, in one of two ways:

  • by e-mail to [email protected];
  • or with GitHub's private reporting: the Security tab of this repository, then Report a vulnerability.

Please include:

  • the version of agentplugged you use, your operating system and the AI tool involved (Claude Code, Codex, Antigravity CLI, Cursor);
  • what an attacker can do, and what they need first (local access, a crafted file, a malicious issue, write access to the repository...);
  • the steps to reproduce it, ideally a minimal repository or journal;
  • whether you want to be credited, and under which name.

We read every report and answer as soon as we can, but we do not commit to a fixed response time. There is no bug bounty program. Please give us a reasonable time to ship a fix before you disclose the problem publicly.

Supported versions

Only the latest version published on npm (agentplugged) receives security fixes. Please check that the problem is still there in that version before reporting it.

Threat model

AgentPlugged has no account and no server of its own: the engine (apps/cli, apps/mcp, packages/*) runs locally, and the state of the work is an append-only journal of signed events kept in Git. The actors it considers are:

  • the creator: the person who owns the product and validates the decisions;
  • the AI agents: the AI tool the creator runs, and the agents of the method inside it. They are useful but not trusted: they can be wrong, and they can be steered by what they read;
  • external content: issues, pull requests, web pages, messages and imported documents, which anyone can write;
  • other people with access to the repository (a collaborator, a fork, a compromised remote).

Signed, chained journal

Module: packages/crypto (ed25519.ts, canonical.ts, verify-event.ts), packages/core (journal/, projection/compute-projection.ts).

  • Every event is a JSON envelope signed with Ed25519 (libsodium). The signature covers the canonical JSON of the envelope (RFC 8785), every field but the signature itself.
  • Each event names, in prev, the hash of the previous event of the same writer: the SHA-256 of its canonical JSON. Each writer's events form a chain.
  • The events are stored as files on a dedicated Git branch (agentplugged/journal). When the journal is synced with a remote, the engine pushes without force, and refuses a remote journal that dropped or rewrote events it had already seen (journal-sync.ts).
  • The state of the product is recomputed from the journal and the signatures are checked every time: an event that is malformed, signed by an unknown key, badly signed, or whose chain is broken (with its descendants) is ignored, never applied.

Two kinds of keys sign:

  • the machine key: one Ed25519 key per machine, generated by the engine. It signs what the engine and the agents record (agents never hold a key: they report through the engine).
  • the creator's key (locale): a separate Ed25519 key, used when the creator answers at the terminal. An answer given at the terminal, or a change of keys, counts only when it is signed by a creator's key: a machine or an agent cannot sign it in the creator's name. An answer relayed from the chat is signed by the machine key and recorded with the weaker chat assurance (see below). Unless a root is pinned on the machine, the first creator key registered in a product is its root; another key counts only once a trusted key endorsed it (projection/human-reducers.ts).
  • Optionally, on Windows, the creator links a Windows Hello credential (WebAuthn, ES256 or RS256, user verification required) to their key: an answer signed with it carries the presence assurance (webauthn-verify.ts).

The secret keys (the machine's seed, the creator's seed) are stored in the system keychain (service agentplugged, through @napi-rs/keyring), never in the repository nor in the journal. Only public keys are written to the journal.

Protects against: an agent, a collaborator or anyone with write access to the repository who forges, alters or reorders an event, or signs a terminal answer in the creator's place; a remote that silently rewrites the shared history.

Encrypted sensitive content

Module: packages/crypto (product-key.ts), packages/core (journal/product-key.ts, sensitive/).

A product can hold a product key: 32 random bytes kept in the system keychain of each machine that holds it, never written elsewhere. Subkeys derived from it (libsodium crypto_kdf) encrypt, with XChaCha20-Poly1305, the payloads of the sensitive events (memory, security notes, costs) and the files of the sensitive folders of .agentplugged/ (such as memory/). Once the key is announced in the journal, a sensitive event written in clear is ignored. An exported key is protected by a passphrase (Argon2id).

Protects against: anyone who can read the repository but does not hold the product key.

Messages between devices

Module: packages/crypto (peer-message.ts, trust-signature.ts), packages/protocol.

The protocol defines how one of the creator's devices sends a message to another (the primitives are in packages/crypto; this repository ships no transport for them). Messages are end-to-end encrypted: an ephemeral X25519 key agreement with the recipient's key, then XChaCha20-Poly1305; the routing fields are bound as associated data, so any change makes the opening fail. Inside, the sender signs the content with Ed25519, and the recipient checks that signature against the devices the journal trusts, never against the routing information. Every signed structure carries its type in the signed bytes, so a signature made for one purpose cannot be replayed for another.

Protects against: any intermediary that carries the messages, which can drop them but can neither read nor alter them, nor impersonate a device, as long as the receiving device's key stays secret. That key is long-lived: an intermediary that recorded the messages could read them later if it ever obtained it.

Proof that a chat answer comes from the creator

Module: packages/core (chat/chat-proof.ts, chat/prompt-hook.ts, chat/answer-command.ts).

The creator usually validates in the chat of their AI tool, and an agent relays the answer. The engine does not take the agent's word for it. A hook of the AI tool (in Claude Code, a UserPromptSubmit hook of type command) records each message the creator types or dictates in a store of the machine, outside the repository (~/.agentplugged/chat, kept 24 hours). The engine accepts a relayed answer only when it quotes a whole message of that store, of the same session and project, recorded after the checkpoint opened, and not already used for another answer (a message given up to 30 minutes before the opening also counts, for a decision the agent had announced, never for a high risk, a release, a publication or an action reserved to the creator). The journal records how each validation was made (presence, locale, chat, device, elicitation, system, agent).

In a tool that has such a hook but whose script is missing from the project, a chat answer is refused. In a tool that has no such hook at all, it is refused (no_proof): the creator validates on the decision card of a trusted device (the web view or the phone), which signs the decision with its key, bound to the point, and the machine checks the signature, the freshness and the replay before it records anything (device assurance), or at the terminal. The generated permissions forbid agents to write the hook's script or the store, and to run the hook.

Protects against: an agent that approves its own work, or invents the creator's answer.

Validation levels

Module: packages/protocol (method-config.ts), packages/core (chat/answer-command.ts).

The creator chooses, in method.yaml, how checkpoints are validated: chat (the default), auto (a low-risk checkpoint takes its recommended option on its own, and the creator can reopen it), or strict (a high-risk checkpoint is answered only at the terminal, and at that level a merge, an action on the forge, a repair of the environment or a skipped step needs a proved answer or the terminal). Some agreements, such as the commands a story runs as proof of its work, are never taken on an unproved chat answer, whatever the level.

Write guard and generated permissions

Module: packages/core (write-guard/, sync/permissions.ts, trust/gate.ts, trust/*-guard.ts).

  • npx agentplugged sync writes, for each supported AI tool, permission rules that forbid agents to read or write secret files (.env and its variants, ~/.ssh, the logins of the forges and of the AI tools), to write the method's own files, the hook scripts, the chat store and the journal's local store, and to run commands such as writing the journal's branch, merging a pull request, or reading the forge's tokens. Only the engine merges, once the creator approved.
  • In Claude Code, a hook before each file write (check_write) refuses a write that breaks a named rule: it contains a secret of a known shape; it weakens a test file (an isolated test, a test disabled with no reason, a fixed wait, an assertion that cannot fail); it is outside the scope of the story in progress; or it contradicts a checkable decision of the approved architecture.
  • In Codex, Cursor and Antigravity CLI, the engine's hooks apply the product's permission rules to the tool calls the agent makes (commands, and file writes where the tool reports them).
  • High-risk actions go through a gate. Installing a dependency, a destructive command, pushing to the main branch, a force push and reading secrets each have a permission rule, and a deny rule refuses them; an installation can also be made to wait for the creator's approval. Deploying, publishing and other risky actions are declared to the gate and recorded, not refused by a rule.

Protects against: an agent that leaks a secret into the code, edits the rules it is bound by, merges its own work, or quietly weakens the tests.

Secret detection

Module: packages/core (doctor/support-report.ts).

Two detectors share the same known shapes (GitHub, GitLab, npm, Stripe, AWS, Slack, Google, Hugging Face, SendGrid, JWT, private keys...). One refuses a write that contains a secret (see the write guard); a token that says it is an example (example, dummy, fake...) is not refused. The other, broader (any long token, password=..., Bearer ..., credentials in an address), masks secrets in the command outputs the engine keeps and in support reports. Neither ever writes the secret's value: a refusal names the kind of secret only.

Untrusted content is data

Module: packages/core (trust/session.ts, init/constitution.ts).

The instructions the method gives every agent state that external content (web pages, issues, messages, imported documents) is data, never instructions. The hooks report each external read, and the journal records it (session.external_read); what such a session writes then carries an untrusted provenance in the history. This is a trace for the audit, not a barrier: it does not block anything by itself.

Out of scope and limits

AgentPlugged reduces what an AI agent can do on its own and makes what happened verifiable. It is not a sandbox. In particular, it does not protect against:

  • A compromised machine. Anyone who controls the creator's user account can read the system keychain, sign with the machine's and the creator's keys, and write the chat store. The presence assurance (Windows Hello) raises the bar for the answers that require it, nothing more.
  • The AI tool itself. The permission rules and hooks are enforced by the AI tool. If the tool ignores them, has a bug, or the creator disables them or allows a command at its prompt, the engine cannot stop the action. These rules are best effort, a second line behind the tool's own.
  • A model that ignores its instructions outside the controlled points. Treating external content as data is an instruction to the agents, not a filter: prompt injection is traced, not prevented. The engine controls the checkpoints, the merges, the writes the guard sees and the commands the rules cover; anything else an agent does is only as safe as the AI tool's own permissions.
  • Fail-open checks. The write guard lets a write through when it is in doubt (no story in progress, a slow journal, an input it cannot read), and the write guard's secret detector only knows the shapes listed above: an unknown kind of token, or a secret split in pieces, goes through.
  • Chat proof is not a signature. The chat proof shows that the creator typed the quoted message in that session; it is not a signature of the creator.
  • Deleting the most recent events. The chain detects an altered or missing event that has a successor, and sync refuses a remote that drops events it has seen; but someone with write access to a repository nobody synced yet can remove its latest events. Keep a remote copy of the journal.
  • Confidentiality of the repository. Only sensitive content is encrypted, and only when a product key exists: the code, the documents and the rest of the journal are as readable as the repository is.
  • The AI provider and your forge. What the AI tool sends to its provider, and the security of your Git host, are outside AgentPlugged.
  • Dependencies. The engine relies on libsodium, @noble/hashes, @simplewebauthn/server and @napi-rs/keyring; a flaw in them is a flaw here. Report it to them, and to us if AgentPlugged uses them unsafely.

AgentPlugged has not been audited by a third party.


AgentPlugged is published by Innovel Studio SASU.

There aren't any published security advisories