RepoOps
Open the app
04 / AI Security

Investigate exposure with the full context.

Review security findings alongside connected runtime evidence, repository changes and captured AI interactions. Inspect the affected code, developer attribution and controls before acting.

Controls enforce only where explicitly armed and supported. Source coverage is separate from policy state.

What it does

Watch, find, trace, contain, fix, prove.

Six jobs, in the order an incident moves through them. The first five are on this page; the proof has a page of its own.

  1. 01Watch
  2. 02Find
  3. 03Trace
  4. 04Contain
  5. 05Fix
  6. 06Prove

01 Watch

A coding agent that writes a transcript on this machine leaves a record, and the detectors read it. The coverage card says which OWASP families your capture tier can see and which it cannot, by name.

How the detectors work

02 Find

The code an agent writes gets read before it ships. RepoOps runs the static analyzer you already trust over the agent's own diff, then joins each finding to the session and the dollars behind it.

How the scans work

03 Trace

A finding names a file and a line. RepoOps names the developer, the session, the prompt cycle and what that session cost, with the confidence stated rather than implied.

How a cause is established

04 Contain

The controls that act on a run ship off. You arm each one, so RepoOps never blocks or kills a run behind your back, and a policy fault degrades to no enforcement rather than to a blocked call.

What arming a control does

05 Fix

A finding at or above your build floor becomes a pull request that builds, passes its tests, and carries a test that fails on the commit that introduced the defect. Nothing merges without you unless you turn auto-remediation on.

How a finding becomes a pull request

06 Prove

Afterwards, authorship is signed with your install's own key, the signals map to the frameworks your auditor asks about, and a standing red-team suite attacks the defenses in CI on every change.

Security as proof

01 / Watch

Inspect the captured record before you judge the run.

Every coding agent on this machine writes a transcript as it works. RepoOps runs rule-based detectors over that record, states which OWASP LLM categories your capture tier can see, and lists the MCP servers and long-lived keys an agent could reach. It reads. It changes nothing, and a gap in capture is shown as a gap.

Session signals docs

Nine detector families over the transcript

Credential exposure, risky commands, prompt-injection markers, exfiltration shape, tool poisoning, system-prompt leakage, excessive agency and unbounded consumption run on every install. A ninth, live egress, fires only while the optional Tap records outbound requests. Each family is a declarative rule or a predicate. No model reads your transcript to decide.

lib/session-signals.mjslib/session-signals-rules.mjs

Coverage stated at your capture tier

The matrix maps each family to its OWASP LLM Top 10 category and, where one fits, a MITRE ATLAS technique, then reports what this machine's capture tier covers. Four categories are out of scope and are listed by name with the reason. The gap is part of the page, not a footnote.

lib/session-signals-coverage.mjs

An inventory of the MCP surface

Every MCP server configured across Claude Code, Claude Desktop, Cursor, Windsurf and VS Code, read from the config files. No server is run to check it. Environment variable names are recorded; values never are. Drift alerts, a pinned allowlist, canary honeytokens and a red-team runner sit on top of the inventory.

lib/mcp-config-inventory.mjslib/mcp-security/drift-alerts.mjs

The long-lived keys an agent can reach

A coding agent runs with your API keys in its environment. RepoOps records one row per secret, plus an over-exposure note when a short run is backed by a long-lived token. A row carries a hash prefix, the pattern type and the metadata. It never carries the key.

lib/nhi/inventory.mjs

Findings that outlive the scan window

The scanner recomputes over a bounded window and transcripts rotate, so a signal from six weeks ago would be gone. The durable store writes each finding down. That is what lets a case still point at it next month, and what puts a recurrence count beside a pattern you have seen before.

lib/security-findings.mjs

One batched alert per tick

A periodic scan diffs its result for new critical and high signals, skips the ones you muted and the ones already sent, and sends one batched alert to the sinks you configured. It reuses the alert senders that already ship rather than adding a second set of clients.

lib/session-signals-alert.mjslib/session-signals-suppress.mjs
Three cases

A production key pasted into a prompt. A developer pastes a live key into a prompt so the agent can reproduce a failing API call, then forgets it was ever there. The credential-exposure family matches the secret shape in the prompt and writes a row masked to its first and last four characters, joined to the session that held it. You know which key, which session and which day, so the rotation is scoped. The finding carries no secret to leak a second time.

A tool result that gives the agent orders. A fetched page or a dependency README comes back through a tool call carrying instruction-shaped text aimed at the model rather than at you. The prompt-injection-marker family fires on instruction-shaped text arriving in a tool result. That is the direction that matters: the same words in your own prompt are a prompt. The captured text is evidence of an attempted instruction, and it grants the agent nothing. You see the run in which the agent was addressed by its inputs, and you can decide whether anything it wrote afterwards can be trusted.

Permissions that widened halfway through a run. A session starts with a careful permission posture, hits friction, and ends with a path moved from ask to allow. Per-session permission capture records timestamped change points, and the mid-session pass fires on a real baseline-to-latest relaxation with the concrete before and after. A widening you can see and argue with. With no baseline to compare against, nothing fires, so the alert means what it says.

Where it stops

It reads. It does not act. A detector firing changes nothing on its own. Blocking a tool call, stopping a run or pinning egress belongs to Contain, and every one of those controls ships off until you arm it.

Four OWASP categories are out of scope. Data and model poisoning is a training-time risk, improper output handling belongs to the app downstream, vector and embedding weaknesses live in a retrieval layer, and misinformation is a truth claim about generated text. The matrix names all four with these reasons.

Live egress needs the optional Tap. Two rules read outbound request metadata a passive transcript scan cannot see. Without the Tap store, which is the default, that family produces no signal rather than a clean verdict.

Rules, not judgment. The detectors are deterministic, so a paraphrase no rule describes goes unmatched. An opt-in semantic layer widens a few families to catch obfuscated secrets and reworded injection. It is off unless you switch it on.

02 / Find

A finding names a line. Read the change around it.

RepoOps does not build its own static analyzer. It runs the one you already trust over the diff the agent produced, checks the packages it reached for, catches a secret as it is written, and reads the screenshots too. Then it joins each finding to the session behind it and to what that session cost, so you inspect the unsafe change with its context rather than a line number.

Agent code scan docs

Semgrep over the agent's own diff

Scanning the whole repository buries what changed under what was already there. RepoOps scopes the scan to the agent's working-tree diff against your main branch and shells out to Semgrep OSS, so you get the upstream rule set and its maintenance. With Semgrep absent, a built-in pass runs a few high-signal rules over the same diff, and the result names which engine produced it.

lib/agent-diff-scan.mjs

A package name the model invented

Every dependency an agent adds is checked against the registry before anyone installs it, so an invented or squatted name is named as one. A registry RepoOps cannot reach degrades the row to unverified. It never degrades to safe.

lib/dependency-provenance.mjs

A secret caught at write time

The gate runs the content of a generated file through the same pattern table the redactor uses, so there is no second list to drift. A blocked write keeps the match types, the counts, a fingerprint and a redacted preview, and hands back the secret-free view of the file.

lib/secret-gate.mjslib/redact.mjs

The screenshot is a text channel

An agent that pastes a terminal or a dashboard screenshot moves a secret into a channel no text detector watches. The image is read with local OCR and the extracted text goes through the detectors that already ship, rather than a second set written for images. The standard installer leaves the OCR engine out to stay small, so there every image reads as unknown, not clean; running RepoOps from source includes it.

lib/multimodal-secret-scan.mjs

The session and the cost beside the finding

Each finding carries the session that produced the diff and what that run cost. The uncommitted diff is one change, so the match is on recency across the whole scan and the surface says so rather than implying a per-line trace. An unpriced model reads unpriced instead of borrowing another model's rate.

lib/security-cost-fusion.mjs

A rollup a team lead can read

The three local scans over what agents write publish a redacted snapshot, so someone who is not sitting at the machine can see the shape of what is being found without the code it was found in. The scanning still happens locally; only the summary moves.

lib/agent-code-findings-snapshot-publisher.mjs
Three cases

The package that does not exist. An agent adds an import for a helper library it invented, and the name is close enough to a real one that nobody reading the diff blinks. Dependency provenance checks the name against the registry as part of the scan and flags it before an install command is ever run. You catch an invented dependency at review time rather than discovering that someone else registered the name and shipped code inside it.

A credential written into a config file. An agent scaffolds a deployment config and fills the auth field with the real key it found in the environment. The write-time gate matches the secret shape as the file is generated, blocks the write, and records the match types, counts and a fingerprint without keeping the value. The key never reaches the working tree, so there is no commit to rewrite and no history to purge. The record of the catch is safe to share.

An expensive run that also wrote bad code. A long agent session burns through a noticeable amount of budget and lands a diff nobody has read closely. The scan runs over that diff, and every finding arrives carrying the session and the metered cost of the run that produced it. One row that answers both questions your manager will ask: what did it get wrong, and what did it cost to get it wrong.

Where it stops

The session join is a correlation. The uncommitted diff is one change, so every finding in a scan carries the same session, the most recent one in the window, with that session's whole cost. It is not a per-line trace, and each finding says so on its face.

Semgrep is an external tool. RepoOps shells out to Semgrep OSS rather than bundling a scanner. Without it installed you get the built-in high-signal pass over the same diff, labelled as such, and the install command for the full rule set.

It finds. It does not block. These scans report. Stopping a tool call before it runs is the gate on the Contain page, and that gate ships all-advisory with its hook unregistered.

A clean scan is not proof of safety. A rule set catches the classes it describes. RepoOps reports what the scanner found over the diff it read, and nothing on this page claims that a repository with no findings has no vulnerabilities.

03 / Trace

Blame names a commit. It does not name an actor.

When a person wrote every line, the commit was the actor: one author, one intent, one head to ask. Now a session with a prompt behind it wrote the line and a person pressed merge. RepoOps rebuilds the missing half of that record, so a finding resolves to the developer, the session, the prompt cycle and what the run cost, with the confidence stated rather than implied.

The full reference architecture

A stamp on the commit, from day one

A prepare-commit-msg hook writes a Session-Id trailer onto each commit, and a second trailer carries the agent and human line counts. It goes in at setup, because a trailer that starts appearing in month three cannot attribute month one. If another hook already sits there, the installer refuses rather than overwriting it.

lib/attribution/install-trailer-hook.mjslib/causal/session-trailer.mjs

A security finding, with no clock involved

A secret committed six months ago is detected today, so closeness in time is not evidence here at all. This leg correlates by commit trailer or by blame, with no deploy hop, and it takes the developer from the commit rather than from whoever owns the machine.

lib/causal/security-attribute.mjs

A production error, back through the deploy

The error names a culprit file, the nearest preceding deploy names a commit, the commit names a session, and the prompt cycle that touched that file names the turn. A slow route walks a shorter version of the same road, correlated on three axes with the purely temporal one banded as the weakest.

lib/causal/prod-attribute.mjslib/causal/prod-correlate.mjs

Four bands, and only two of them are proof

Exact means the line sits inside a hunk of a commit carrying a session id and that session is the only candidate. Strong means blame traces the line to a commit whose window overlaps one session that touched the same file. Weak reads as possibly and never counts. None is not written down at all.

lib/causal-link.mjs

The dollars on the same row

The session that wrote the line has a metered cost, so the answer carries it. A model with no price on file reports as unpriced rather than borrowing a similar model's rate, because a wrong number is worse here than a missing one.

lib/security-cost-fusion.mjs

A configuration change that explains a symptom

Four symptoms are watched, cost up, quality down, refusals up and latency up, against five kinds of change that can explain one: a model swapped, a prompt changed, a tool added or removed, a parameter changed. No coincident change is recorded as unexplained rather than pinned on the nearest edit.

lib/model-drift.mjs
Three cases

The secret nobody remembers committing. A scan turns up a credential in a config file that has been in the repository for months, and three people have touched the file since. The security leg ignores the clock entirely, correlates through the commit trailer or blame, and reports the developer from the commit with a band on the answer. A rotation conversation with the person who actually introduced it, or an honest weak band that tells you the evidence does not support naming anyone.

A 3am error in code nobody wrote by hand. Production throws in a file that was generated during an agent session two weeks ago, and the pull request that carried it was reviewed in four minutes. The chain runs incident to deploy to commit to session to prompt, and returns the prompt cycle that touched that file along with what the run cost. You can read the instruction that produced the bug. That is the difference between fixing this error and knowing which instruction to change before the next one.

The bill went up and nothing obvious changed. Spend on a workflow climbs, quality feels worse, and the only candidate explanations are a model swap and a prompt edit made in the same week. Four symptoms are watched against five kinds of configuration change, and the symptom is linked to the change that coincided with it under the same band rules as every other leg. A named change to roll back, or an explicit unexplained verdict that stops the team blaming the last edit somebody happened to notice.

Where it stops

A trailer cannot attribute the past. The hook stamps commits made after it is installed. History written before setup is reachable only through blame, which bands weaker, so the earlier you install it the more of your repository the chain can reach.

Weak is not proof, and the product treats it that way. More than one candidate session, a blame walk that crossed a squash with no recovered trailer, or a rename inside the window all band weak. Weak reads as possibly and is never counted as an attribution.

No attribution rate is claimed anywhere. How much of a repository the chain reaches depends on when the hook went in and how the team commits. Any percentage printed on a marketing page would be a claim about your repository that we are in no position to make.

File-level overlap alone is weak evidence. Matching a prompt cycle to a line through file-level overlap is banded weak on purpose. The strong answers come from the trailer and from a sole-candidate blame match, not from proximity.

04 / Contain

Every control here is off until you arm it.

These are the parts of RepoOps that can act on a live run: a gate that can refuse a tool call, a switch that stops a run in flight, a broker that keeps your real key out of the agent, and an allowlist that pins where a sandboxed run may reach. Each ships off and says so. Armed or not, the record separates what a run attempted from what it confirmed, and a policy fault degrades to no enforcement rather than to a blocked call.

The controls you can arm

A gate on the pending tool call

One pure decision on a single call: allow, block or flag. It rides the agent's own synchronous pre-tool hook for Claude Code, the Codex CLI and the GitHub Copilot CLI, plus the MCP tool-call seam, rather than the capture path. Only a high or critical firing is block-eligible; medium and low warn even when the detector is armed to enforcing.

lib/enforcement-gate.mjsscripts/cc-pretooluse-enforce.mjslib/mcp-enforce.mjs

A kill switch with three legs

Revoke the run's scoped token, signal the agent process, record a block on the run's branch, which the armed PreToolUse gate enforces by refusing the agent's further commits there. No git hook is installed, so a commit made outside an agent session is not stopped. Each leg reports whether it completed and whether anything was verified, and the run is marked contained only when all three did both. Dormant, it still computes the full plan so you can see what arming would do.

lib/run-kill-switch.mjs

A broker between the agent and your key

Instead of a long-lived key sitting in the agent's environment and context, a local broker holds the one real key and mints a short-lived, task-scoped token from it. The raw key never leaves the module, is never logged or stored, and the broker fails closed when no key is set. It listens on loopback only.

lib/broker/credential-broker.mjs

A per-repo egress allowlist

Pin the domains and addresses a sandboxed run may reach. The list maps straight onto the shipped sandbox's per-host network capability, so a pinned host becomes exactly one granted capability and nothing else leaves. A policy that cannot be loaded is treated as the empty list, never as allow-all.

lib/egress-policy.mjslib/sandbox/capabilities.mjs

Forensics for the run you stopped

Over the redacted session record already on the machine, RepoOps builds a timeline, a blast-radius report and a re-read of the transcript (its prompts and a total per tool). The timeline keeps attempted actions apart from confirmed effects, and an incident is reconstructed from local evidence without shipping the raw session anywhere.

lib/run-forensics.mjs

A subagent that attests back to its parent

When work fans out to Task subagents or Workflow legs, which agent in the tree did this has no answer. A signed record carries the parent run, the subagent, the tool and data scope the parent declared, and whether the subagent exceeded it. Verification recomputes the digest from the record's own fields rather than trusting a stored one. Dormant unless attestation is switched on.

lib/a2a-attestation.mjs
Three cases

The command that reads a secret and sends it. An agent composes a single shell command that reads a dotenv file and pipes the contents to an external host, and it looks unremarkable inside a long tool call. The gate matches the read verb, the secret path and the outbound call as one pairing on the pending call, before the tool runs. Armed, it blocks. Advisory, it flags and records the attempt. Either the call does not happen, or you hold a specific record of the call that would have sent a credential, with the reason attached.

A run going wrong while you watch it. A long agent run starts making changes nobody asked for, and every minute it keeps working the cleanup gets bigger. The kill switch runs its three legs in order and reports each: token revoked, process signalled, branch block recorded, so the armed PreToolUse gate refuses the agent's further commits. The blast radius stops growing while you read the timeline. A leg that could not complete is named, so you finish that part by hand.

A sandboxed job that should only talk to two hosts. A scheduled agent run needs your package registry and your own API, and nothing else, but the sandbox it runs in can reach the whole internet. Pin those two hosts in the repository's egress allowlist. They become exactly two granted network capabilities on the shipped sandbox, and the daemon hook fires when a run reaches for anything else. An unlisted host is an alert rather than a quiet outbound request, and a policy you cannot load closes the sandbox instead of opening it.

Where it stops

A default install enforces nothing. The gate ships all-advisory and its hook is not registered. No page here may describe RepoOps as protecting a machine it has not been armed on, and this one will not.

It sees agents that expose a hook. Enforcement runs on the Claude Code, Codex CLI and GitHub Copilot CLI pre-tool hooks and the MCP seam. There is no network interception here and no plan for one, because inspecting your agent's traffic would contradict the rest of this.

The approval hold is inert today. Two detectors can raise a hold when armed to enforcing, and the human approver that would release one is not wired yet. With no approver a hold degrades to a flag with its reason rather than hanging a call nobody can release.

It is not tamper-proof. The armed state is a config file, and anything running with your privileges can change it. Prompt injection is also unsolved at the model layer: a captured instruction is evidence, never permission, and these controls bound the blast radius of an agent that has been talked into something. They do not claim to stop every attack.

05 / Fix

The fix is already written. You decide if it ships.

A security finding at or above the floor you set becomes a pull request that builds, passes its tests, and carries a regression test that fails on the commit which introduced the defect. Five checks run before you see it. Then it stops and waits. Auto-remediation is off until you turn it on; with it off, nothing merges without you.

The whole loop, stage by stage

A brief that works as a leash

Before anything is written, the finding is scoped into a brief that names the files a fix may touch. The builder is held to it: every changed file must be one the brief named or a new test sitting beside it. A fix that wanders outside its scope is not shown to you.

lib/remediation/build-verify.mjs

Five checks, and no partial credit

It builds. The tests pass. A new regression test passes on the fix and fails on the commit that introduced the defect. The guard holds. The pull request's own CI is green. A fix that misses one goes back to the builder once with the failure attached, and a second miss stops with both attempts recorded.

lib/remediation/verdict.mjs

One decision, re-checked as you press it

Approve or reject. On approve the whole bar is re-run server-side at that moment rather than trusted from when the card was rendered, so a stale green cannot merge anything. Reject requires a reason, and the diff, the guard and the lesson are discarded together.

lib/remediation/verdict-gate.test.mjslib/remediation/builder.mjs

The lesson lands before the merge

Approving writes the lesson first and merges second, because the lesson is reversible and the merge is not. For a security case that carries a catalog rule, the same move arms that detector's own pattern. Whether the class recurs is assessed afterwards, and until captured sessions exist in the window it reads not assessable.

lib/causal/prevention-receipt.mjs

A build cannot start on your laptop by surprise

Builds run as an agent session in the cloud, one invocation per tick. The local queue worker skips build jobs outright, so a queued build waits for that routine rather than firing on the machine you happen to be working on.

lib/remediation/build-routine.mjs

Several findings, one ordered plan

Pick a few things off the queue and get a single plan rather than a pile of pull requests: grouped by what a change touches, foundations first, and the work that needs a human kept separate from the work that does not.

lib/remediation/combine.mjs
Three cases

The same injection bug, in the fourth repository. A pattern your team has fixed three times before turns up again in a service nobody has touched this quarter. The finding is scoped, the fix is built and checked five ways, and approving it arms the detector's own rule against that pattern rather than only patching the instance. The fourth occurrence gets a fix and arms the rule. Whether there is a fifth is measured, not promised: recurrence reads not assessable until captured sessions exist in the window.

A weekend finding nobody wants to own. A high-severity finding lands on Saturday. It is real, it is small, and it will sit unassigned until Monday standup. The loop scopes and builds the fix inside the daily cap, runs the five checks, opens the pull request, and stops there unless you turned auto-remediation on and set that class to auto. Monday starts with a finished, CI-green pull request behind one decision, rather than with a triage conversation about a two-line change.

A fix you do not want. The loop builds something technically correct that would be wrong for reasons no rule could know, like a deprecation already scheduled for next sprint. Reject takes a reason, and the diff, the armed guard and the written lesson are discarded together rather than leaving a guard behind for a fix that never shipped. A clean no, with the reason recorded, and no half-applied state to discover three weeks later.

Where it stops

It does not run unattended unless you turn that on. The build floor, the daily cap and the watch-only switch bound what the loop does before the gate. The gate is a person until you turn auto-remediation on and set a class to auto; then the loop merges that class when all five checks pass, and the case carries the receipt.

Five checks are a bar, not a proof. They establish that the change builds, that the tests pass, that a regression test moves both ways, that the guard holds and that CI is green. They do not establish that the fix is the right design, which is why the decision is yours. They are also not proof of prevention: recurrence is assessed later, from captured sessions.

No success rate is claimed here. The available figure comes from a synthetic fixture and needs a paragraph of caveats to be honest, so it does not appear on a marketing page. The loop-health surface in the app measures your own install instead.

It only fixes what the earlier acts found. A finding no detector raised and a defect no chain resolved never reach the loop. Fix inherits the coverage of Watch and the confidence of Trace, and it cannot exceed either.

The limits

What this does not do.

Stated here rather than found out later. The long version, with the module behind each boundary, is on the proof page.

Read the boundaries in full

Nothing is enforcing until you arm it

The gate ships all-advisory and its hook is not registered. A default install of RepoOps enforces nothing, and no page here may describe it as protecting one.

Prompt injection is not solved here

It is unsolved at the model layer. These controls bound the blast radius of an agent that has been talked into something. They do not claim to stop every attack.

It sees agents that expose a hook

The gate runs on the Claude Code, Codex CLI and GitHub Copilot CLI pre-tool hooks. There is no network interception here and no plan for one, because inspecting your agent's traffic would contradict the rest of this.

It is not tamper-proof, and we are not SOC 2 certified

The armed state is a config file, and anything running with your privileges can change it. RepoOps holds no SOC 2 certificate today and says so plainly rather than implying otherwise.

Why the local record

Running locally is not the differentiator any more.

Endpoint security now inspects local AI agents through the same vendor hook interfaces this gate uses. That was the wedge, and we are not going to pretend it still is.

What differs is what the local record lets you prove afterwards. The reconciled dollar sits on the merged pull request. Authorship is signed with your install's own key. The lesson from a defect arms a guard against its recurrence at the merge gate, aimed at the thing that caused it because the chain resolved a session and a prompt rather than a file. Enforce locally, govern centrally, and only redacted metadata leaves the machine.

Start with the free local install.

One developer, no account, nothing leaving the machine. Arm a control when you want one.