Mergen: Enterprise Security Brief
For: Security engineers, InfoSec reviewers, compliance teams Purpose: Answers the standard questions before approving Mergen for internal use Version: 1.4.x · Last updated: 2026-08-09
One-paragraph summary
Mergen is an inline Execution and Security Gateway for AI agents. Before an agent's shell command or file edit executes, a local, deterministic policy engine ("Gate A") evaluates it in under 1ms and either passes it, blocks it with a structured explanation, or holds it for human approval (Slack, VS Code, or CLI). No LLM sits in that enforcement path. Team deployments add RBAC, SSO-federated agent identity, an ephemeral cloud credential broker (AWS/GCP/Azure; no long-lived cloud secrets in agent config), and a hash-chained, tamper-evident audit trail. Mergen also includes a local debugging/observability layer (the sensor/capture stack this brief originally described) that binds only to 127.0.0.1 and makes no outbound connections during normal use. Everything that touches your data (the sensor layer, the policy engine, the credential broker) is MIT open-source and auditable at the repository this brief ships alongside. The one closed-source component is the diagnostic hypothesis engine (analyze_runtime's LLM-backed root-cause analysis), never part of the enforcement path, and not required for Gate A, HITL, RBAC, SSO, or credential brokering to work.
What leaves the machine
| Event | Destination | When | Can be disabled |
|---|---|---|---|
| License key validation | api.lemonsqueezy.com |
On activation + weekly background re-check | Don't activate a paid license |
| LemonSqueezy billing webhook | Inbound only | When you purchase | N/A |
analyze_runtime Context Pack |
LLM API (OpenAI / Anthropic) | Only when you explicitly call analyze_runtime |
Use only free tools |
| HITL hold delivery | Slack (if configured) | Only when a HOLD-tier policy match occurs | Don't configure MERGEN_SLACK_BOT_TOKEN; the hold is still placed and logged locally either way |
| Ephemeral credential issuance | AWS STS / GCP sts.googleapis.com + iamcredentials.googleapis.com / Microsoft identity platform |
Only when an agent requests a configured credential scope | Don't configure ~/.mergen/credential-scopes.json |
| Mergen's own OIDC issuer | Inbound only: GCP/Azure fetch /.well-known/jwks.json from MERGEN_PUBLIC_URL to verify Mergen-signed federation tokens |
Only when GCP/Azure credential scopes are configured | Don't configure GCP/Azure scopes; AWS brokering needs no public exposure at all |
| Inbound webhooks (PagerDuty, GitHub, Sentry) | Inbound only, signature-verified | Only if configured | Don't configure the corresponding secret |
| SIEM/audit forwarding | Your configured SIEM endpoint | Only if MERGEN_SIEM_* is set |
Off by default |
| Usage telemetry (anonymous installId, tool call counts) | MERGEN_TELEMETRY_URL |
Off by default. Requires POST /telemetry { enabled: true } AND MERGEN_TELEMETRY_URL env var AND 24h throttle has elapsed |
Never set MERGEN_TELEMETRY_URL |
| npm version check | registry.npmjs.org |
Once per 24h, version number only | MERGEN_NO_UPDATE_CHECK=true |
Everything else (console logs, network events, stack traces, DOM state, localStorage, backend stdout, the content of commands/edits Gate A evaluates) stays on 127.0.0.1. It is architecturally impossible for it to leave without modifying the source code or deliberately configuring one of the integrations above.
Network architecture
AI IDE (Claude Code / Cursor / Windsurf / VS Code)
Bash/Edit tool call
│
▼
Mergen Execution Gateway (Gate A) ← <1ms deterministic policy eval,
│ no LLM in this path
PASS / BLOCK / HOLD
│
├── PASS → handler runs
├── BLOCK → structured error, nothing executes
└── HOLD → suspended; Slack/VS Code/CLI approval resumes it
Chrome extension (legacy sensor/capture layer)
content.js POST http://127.0.0.1:3000/ingest
background.js GET http://127.0.0.1:3000/health (badge polling, 10s interval)
Mergen server
HTTP listener 127.0.0.1:3000–3010 (loopback only, not 0.0.0.0, by default)
IDE / AI agent
the Bash/Edit PreToolUse hook (Claude Code, Windsurf) or self-heal watcher
(Cursor, plain VS Code) calls the local gate directly, never over the
open internet
The server cannot be reached from another machine on the local network by default. MERGEN_BIND=0.0.0.0 is the team-mode override, off by default, requiring deliberate configuration. Cloud mode (MERGEN_CLOUD_MODE=true, tenant-isolated Postgres-backed telemetry/CI-gate/credentials) is a separate, currently-unproductized deployment target; see this brief's "Deployment options" section.
RBAC (role-based access control)
server/src/sensor/rbac.ts + routes/rbac.ts. Members are registered with a role (POST /rbac/members); Gate A policy rules can condition on conditions.roles (enterprise-policy-engine.ts), so an operator can write, for example, "developers may git push, only release-managers may git push --force" as an ordinary policy rule rather than a hardcoded exception. GET /rbac/me resolves the effective role for the current request. RBAC roles combine with an agent's own agentId-scoped conditions: a policy rule can match on either, both, or neither.
SSO federation
server/src/sensor/sso.ts. Two methods, tried in order: real OIDC (MERGEN_OIDC_ISSUER_URL + MERGEN_OIDC_CLIENT_ID), where the bearer token is verified as a JWT against the issuer's own JWKS (signature, iss, aud, exp via the jose library), not a shared-secret string compare, falling back to a shared bearer token (MERGEN_SSO_TOKEN) for solo/local deployments without an OIDC provider. A verified OIDC identity's email/preferred_username claim overwrites the caller-supplied member header, so downstream RBAC resolves a role for a cryptographically-verified identity, never a self-asserted one. MERGEN_OIDC_GROUP_ROLE_MAP can auto-provision RBAC roles from the token's groups claim. Separately, agent-identity.ts can bind a human's SSO-verified identity into a signed, HMAC-issued agent token (mergen-server agent-register --user=<email>), so every audit-ledger row an agent produces is attributable to a real person, not just an agent id.
Ephemeral cloud credential brokering
Instead of a long-lived AWS access key, GCP service-account key, or Azure client secret sitting in an agent's configuration, agents request short-lived, narrowly-scoped credentials on demand:
- AWS: real STS
AssumeRole, using Mergen's own ambient AWS identity (whatever credentials themergen-serverprocess itself runs as) to assume an operator-configured role. No OIDC exchange on this path. - GCP: real Workload Identity Federation: Mergen signs a short-lived OIDC token (its own issuer, below), Google's STS exchanges it, then the IAM Credentials API impersonates a narrowly-scoped service account. No GCP key ever exists.
- Azure: a real Entra client-assertion (JWT-bearer) flow. The real Azure access token never reaches the caller; Mergen proxies API calls through a server-side session instead, which is what makes Azure the one provider where Mergen can cut off a caller's access mid-flight (see Revocation below).
Every credential request is routed through Gate A before issuance: an operator can write an ordinary policy rule restricting which agents/scopes may request which credentials, exactly like any other tool call, including holding a request for human approval. Every issuance, failure, and revocation attempt is written to the hash-chained audit trail described below (never the credential value itself).
Revocation, stated precisely, not uniformly, because it isn't uniform. None of the three clouds expose an API that revokes the actual bearer credential once issued; this is a structural property of short-lived client-credential/service-account/session tokens in all three clouds, confirmed against each provider's own documentation, not a Mergen gap. Azure is the partial exception: because Mergen never hands the real token to the caller, Mergen's own proxy session can be torn down immediately, which cuts off the caller's access, but the real Azure token Mergen was holding server-side stays valid at Azure until its own natural expiry. AWS and GCP have no revocation path at all once issued; an attempt is still logged (the attempt itself, someone believing a live credential needed killing, is a meaningful signal on its own), and the honest revoked: false is returned rather than a false success.
Mergen's own OIDC signing key (the one that lets GCP/Azure trust Mergen-issued tokens) is persisted with 0600 file permissions inside a 0700 directory, with permissions re-verified and tightened on every server start (not just at first creation), and an env-only override (MERGEN_OIDC_SIGNING_KEY) for production deployments that don't want it on disk at all.
Independent validation status, stated explicitly rather than implied by "production-ready":
- Credential brokering is a production-grade implementation, verified against a checklist (policy-gated issuance, full audit trail including failures and revocation attempts, provider-specific revocation semantics, credential/error redaction, format validation, concurrency-safe scope storage).
- It has not yet been independently security-reviewed. A scoping brief exists for exactly this engagement, naming every file in the trust boundary and the specific adversarial questions worth a reviewer's attention.
- It has not yet been verified end to end against real cloud provider infrastructure: the automated test suite deliberately mocks every provider network call. Runbooks exist to close this for AWS and GCP against real, throwaway cloud resources; neither has been executed yet, and Azure has neither a runbook nor a live run.
These three claims are kept separate on purpose: "credential brokering is not an MVP" is true; "credential brokering has been independently validated end to end" is not, yet.
Execution enforcement details
- Deterministic only. Every BLOCK/HOLD/PASS decision traces to a rule a human wrote or approved, never to a model's judgment. Machine-learning or similarity-scoring signals (e.g. historical-incident similarity) may only ever escalate a PASS to a HOLD; they can never independently produce a BLOCK or a PASS.
- Hash-chained audit trail. Every intercepted action (
GET /agent-blunders) carries a SHA-256 hash of its content plus the previous entry's hash: any deletion or modification of a historical entry breaks the chain and is detectable by an external auditor without trusting the server. - Fail-closed by default. A local rule-evaluation fault, a stale heartbeat, or an unparseable payload blocks or holds rather than silently passing. The one intentional fail-open path is an operator-activated, time-bounded panic bypass, itself logged loudly.
- Works fully offline. Gate A's PASS/BLOCK/HOLD decision is always a synchronous, local computation; cloud/Postgres reachability is never required to produce one.
Chrome extension permissions
| Permission | Why it's needed | What it accesses |
|---|---|---|
storage |
Persist port config and per-tab mute state | chrome.storage.local, no page content |
tabs |
Send mute-toggle and port-change messages to content script | Tab ID only, not content |
alarms |
Schedule 10s health-poll interval | No data access |
host_permissions: http://127.0.0.1:3000–3010/* |
POST captured events to local server | Only the loopback, no external URLs |
The extension has no permissions to access external websites, read cookies from other origins, or make requests to anything outside 127.0.0.1. The manifest can be reviewed at extension/manifest.json.
The content script (extension/src/content.js) runs at document_start in every tab. It:
- Wraps
console.log/warn/errorto forward args to the local server - Wraps
fetchandXMLHttpRequestto capture request/response metadata - Captures
localStorage/sessionStorageonconsole.errorevents only - Never modifies page behavior: all patches call the original function first
- Fails silently: every code path is wrapped in
try/catch; an error in the extension cannot affect the page
Data captured and stored
| Data type | What is captured | Where it's stored | Retention |
|---|---|---|---|
| Console events | level, args[], stack, url, timestamp |
In-memory ring buffer (2,000 events) | Cleared on server restart or clear_buffer |
| Network events | method, url, status, duration, request/response body (≤8KB) |
Same ring buffer | Same |
| DOM context | url, title, activeElement, component name, localStorage keys/values |
Same ring buffer | Same |
| Backend stdout | Process name, line text, timestamp |
Same ring buffer | Same |
| SQLite history | Console errors only, for anomaly baseline | ~/.mergen/history.db (WASM SQLite, in-process) |
Configurable via MERGEN_RETENTION_HOURS |
| Agent Blunder Log / action ledger | Every PASS/BLOCK/HOLD verdict, hash-chained | ~/.mergen/agent-blunders.json, ~/.mergen/action-ledger.db (workspace-scoped) |
Configurable via MERGEN_LEDGER_RETENTION_DAYS; MERGEN_ZERO_RETENTION=true disables local persistence entirely |
| License state | Key hash, plan, customer email | ~/.mergen/license.json |
Persists until manually deleted |
| Local secret | A random UUID | ~/.mergen/secret (mode 0600) |
Persists; used to authenticate local requests |
| Credential scope config | Role ARNs / service account emails / app client IDs (never credentials or secrets) | ~/.mergen/credential-scopes.json (mode 0600) |
Operator-managed; deleted by editing the file directly |
No data is written to any path outside ~/.mergen/ and the in-process SQLite store.
PII redaction
Automatic, always-on, runs before any data enters the ring buffer or an audit/log record:
| Pattern | Action |
|---|---|
Authorization: Bearer ... header |
Value replaced with [REDACTED] |
Cookie: header |
Value replaced with [REDACTED] |
password, secret, token, api_key object keys |
Value replaced with [REDACTED] |
JWTs (eyJ... base64 patterns) |
Replaced with [REDACTED:jwt] |
| AWS access keys / secret keys | Replaced with [REDACTED] |
| Email addresses | Replaced with [REDACTED:email] |
| Credit card numbers (Luhn-valid 13–19 digit sequences) | Replaced with [REDACTED:card] |
| Network request/response bodies | Hard-capped at 8KB; remainder discarded with […truncated by mergen] marker |
Redaction source: server/src/sensor/redact.ts, open source and auditable. The same function guards upstream cloud-provider error bodies in the credential broker before they can reach an HTTP response or a log line.
Authentication
Local shared secret: a random UUID written to ~/.mergen/secret on first start (file permissions 0600). All state-changing HTTP requests (clear, license activation, telemetry toggle, admin mutations) require the x-mergen-secret header to match.
Per-boot loopback token: a second, per-port token the server writes at startup; every local API request must attach it, closing the gap where a request without it would otherwise 401 every tool call.
Signed agent tokens: agent-identity.ts issues HMAC-signed, per-profile agent tokens (mergen-server agent-register) required on every credential-broker request (x-mergen-agent-token), replacing self-asserted agent identity for anything privilege-relevant.
Admin mutation guard: routes that can materially change enforcement behavior (policy review, RBAC changes, config writes) require the same local secret or MERGEN_ADMIN_SECRET, specifically so a process with only filesystem access (an AI coding agent, for instance) can't forge a policy change or a human-review approval.
This prevents a malicious web page from calling a state-changing endpoint via a cross-origin request; the browser will send the request, but the server rejects it without the secret. The extension reads the secret from the file and includes it on every eligible request.
An optional additional secret can be set via MERGEN_SECRET env var, which is then required on the /ingest endpoint as well.
Air-gap and offline use
All enforcement functionality works with no internet connection, since every PASS/BLOCK/HOLD decision is a synchronous local computation: Gate A, HITL holds (delivery to Slack requires network; the hold itself is placed and logged locally regardless), RBAC, and the audit trail. Credential brokering necessarily requires network reachability to the relevant cloud provider's STS/IAM endpoints when a credential is actually requested, but nothing else in the enforcement path does.
Everything except license validation, analyze_runtime, HITL Slack delivery, and credential issuance works fully air-gapped:
- All local buffer and capture functionality
- The VS Code panel, signals, hypothesis detection
- The CLI (
mergen-server status,doctor,guard,watch,credentials list) - The full Gate A policy engine and HITL hold mechanism (local decisions + local logging)
To run fully air-gapped (no outbound connections at all):
MERGEN_NO_UPDATE_CHECK=true mergen-server start
# Do not activate a paid license key
# Do not enable telemetry
# Do not configure Slack/PagerDuty/GitHub/credential-scopes.json
Deployment options for enterprise
| Mode | Network exposure | Use case |
|---|---|---|
| Default (loopback) | 127.0.0.1 only |
Individual developer workstation |
Team mode (MERGEN_BIND=0.0.0.0) |
LAN, protect with firewall | Shared dev server, pair-programming |
| Docker / container | MERGEN_HOST=host.docker.internal in the container |
Containerised backend services |
| Air-gapped | No outbound, see above | Secure development environments |
| Self-hosted sync plane | Your infrastructure, no cloud dependency | Shared policy/audit across a team's own installs |
Cloud mode (MERGEN_CLOUD_MODE=true) |
Tenant-isolated, Postgres-backed | Not currently productized/sold; internal infrastructure only as of this writing |
Open source auditability
| Component | License | Source |
|---|---|---|
| Browser extension | MIT | extension/ |
| Firefox extension | MIT | extension-firefox/ |
| Node.js SDK | MIT | sdk/node.js |
| React Native SDK | MIT | sdk/mergen-inject.js |
| HTTP ingest server | MIT | server/src/sensor/ |
| Gate A policy engine | MIT | server/src/intelligence/enterprise-policy-engine.ts, tool-guard.ts |
| Credential broker | MIT | server/src/intelligence/credential-broker.ts, gcp-broker.ts, azure-broker.ts, oidc-issuer.ts |
| RBAC / SSO | MIT | server/src/sensor/rbac.ts, sso.ts |
| PII redaction | MIT | server/src/sensor/redact.ts |
| Ring buffer | MIT | server/src/sensor/buffer.ts |
| Agent Blunder Log / action ledger | MIT | server/src/sensor/agent-blunder-store.ts, action-ledger.ts |
| Hypothesis Engine (causal analysis) | Closed source | server/src/intelligence/causal.ts, not distributed; reached via HTTP routes, not part of Gate A's enforcement path |
Everything that materially decides whether an action is allowed (Gate A, the credential broker, RBAC, SSO verification, the audit trail) is open source and in this repository. The closed-source components are diagnostic/advisory only, consistent with this project's own invariant that intelligence never enforces.
Common security team questions
Q: Can Mergen exfiltrate source code?
A: No. The extension captures console output (what developers explicitly console.log) and network metadata. It does not read source files, access the filesystem, or capture keystrokes. The server never accesses source files except to read .map files for stack trace de-minification, and it does not transmit them.
Q: Can a compromised Mergen server read data from other tabs?
A: The server can only receive what the extension sends. The extension is scoped to 127.0.0.1 for outbound requests. The server has no browser API access.
Q: Does the AI (Cursor, Claude, Copilot) see my raw logs? A: Yes, that is the purpose. The AI sees the same console logs and network events that would be visible in Chrome DevTools. It sees what you show it, not more. PII redaction runs before the AI can access any data.
Q: What happens to data when the developer closes their laptop?
A: The ring buffer is in-memory. Server shutdown clears it. No data survives a restart unless the SQLite history store is enabled (off for most events, on for console errors only). The audit trail (Agent Blunder Log, action ledger) does persist across restarts by default; this is intentional, since a governance audit trail that vanished on restart would defeat its own purpose, and can be disabled with MERGEN_ZERO_RETENTION=true.
Q: Can other developers on the same Wi-Fi reach the server?
A: No. The server binds to 127.0.0.1 by default. Other machines cannot reach 127.0.0.1 on your machine.
Q: Can a malicious website make requests to the Mergen server?
A: A website can attempt to POST to 127.0.0.1:3000, and the browser will send the request (CORS doesn't block sends). The server requires the local shared secret and loopback token on all state-changing endpoints, which the malicious website does not have. The /ingest endpoint accepts events without the secret by default: a malicious page could inject fake events, but this would only add noise to the developer's debug session, not exfiltrate data or bypass Gate A enforcement.
Q: Can an AI agent forge its own approval, escalate its own privileges, or bypass a HOLD? A: An agent has the same filesystem access as any local process, which is exactly the threat model Mergen's own signing-key handling is designed against: the admin mutation guard, RBAC changes, and policy edits all require a secret the agent doesn't have by default, and every fail-open path (the panic bypass) is operator-activated, time-bounded, and loudly logged, not agent-triggered.
Q: Is there a way to use Mergen without the Chrome extension?
A: Yes. The Node.js SDK (sdk/node.js) instruments backend services without any browser extension. For frontend, the DevTools snippet (sdk/devtools-snippet.js) instruments the current page via manual injection, with no extension installation required. Gate A enforcement (the Bash/Edit hook and the CLI) has no dependency on the browser extension or capture layer at all.
Contact
Security vulnerabilities: omer@mergen.app GitHub Security Advisories: mailto:omer@mergen.app Response SLA: 48 hours for critical issues