Decoupling Security Outcomes from Headcount: The Case for a Native Ops Layer

The goal every security leader is actually chasing is simple to state and brutal to reach: get security work done end to end — assigned, tracked, verified, closed — efficiently, with automation you can control and measure. Said differently, decouple your program's outcomes from its headcount. A security program shouldn't have to hire a program manager for every N engineers just to keep a spreadsheet honest, and yet that is the tax most programs quietly pay.

The pressure to break that coupling is only getting worse, because the other side isn't paying it. Attackers are scaling their own operations without scaling their teams — cheap, capable open-weight models now do the reconnaissance, triage, and exploit-drafting that used to require people. The cost of coming at you is falling. If the defense still scales linearly with human bookkeepers while the offense scales with GPUs, the math doesn't work. The bar on how efficiently a defensive program has to run just went up, and it went up for structural reasons that aren't going to reverse.

This post is about the piece of the program that has stubbornly stayed manual: not detection, but the operational layer around it — who owns a finding, what its real priority is, whether it's actually fixed. That layer is almost always run as human clerical work, and clerical work is exactly what caps a program at the size of its ops staff.

The spreadsheet nobody can trust

Ask a security leader how they track finding ownership, remediation SLAs, and accepted-risk exceptions across their org, and the honest answer is almost always the same: a patchwork. A Jira board here, a Slack channel there, a compliance dashboard that reports SLA status, and — somewhere in the middle of it — a spreadsheet. One person owns the spreadsheet. It maps GitHub authors to Slack handles, or teams to repositories, or which findings someone has decided to live with for now. Everyone else trusts that it's current. It isn't.

We've watched this pattern break in the field. (The customer stories throughout this post are illustrative composites drawn from real deployments — details are blended and anonymized, but every failure mode described is one we've actually seen.) One security team we spoke with was manually moving Jira tickets between team-specific boards because findings all landed on a single org-wide board and nothing routed them onward. Their lead called the situation unacceptable — it defeated the purpose of having a tool at all. And the data needed to route work automatically, the GitHub-to-Slack identity mapping, was sitting in a spreadsheet held by a single team member. The tool had become a passive filing cabinet, and the real system of record was a file on one laptop.

That's the failure mode. Not spreadsheets specifically — spreadsheets are just the most honest version of the problem. The problem is that program management for security is treated as manual bookkeeping: humans deciding who owns a finding, humans typing an SLA into a cell, humans clicking "done," humans chasing other humans for action. That model doesn't scale — every new engineer adds work the spreadsheet-owner has to absorb — and worse, it produces records nobody can trust. This post is about the alternative: a native ops layer driven by context rather than clerical labor, so the program can grow without the ops team growing under it.

Why bookkeeping breaks before the tooling does

The instinct, when the spreadsheet buckles, is to buy a better tool. A nicer ticketing integration, a dashboard with SLA countdowns, a fancier board. But the tool was never the bottleneck. The judgment behind the tool was.

Consider the three things a security program actually has to know about every finding:

  • Who owns this code? Not who's on-call this week — who actually wrote the vulnerable line, or whose team owns the file path.
  • What's the real risk? Not the scanner's raw severity, but the triaged, validated priority that a developer will believe.
  • What's actually been fixed? Not what a checkbox claims, but what the underlying state confirms.

A spreadsheet holds assertions about these three things. It can't verify any of them. The ownership column is right until someone changes teams. The SLA column is right until nobody looks at the row. The status column says "closed" whether or not the vulnerability is actually gone. Every column is a promise that decays the moment a human stops maintaining it — and at scale, humans always stop maintaining it.

We've seen exactly what that decay costs. A security team told us about a critical RCE fix that sat unreviewed for 35 days, well past any reasonable SLA, and another finding that sat 20 days past SLA and was, in their own words, unlikely to ever be reviewed. Those aren't tooling failures. The SLA was tracked. The dashboard showed it as breached. What was missing was anyone — or anything — whose job was to notice the stale row and act on it. That's the gap a native ops layer fills, and it's a judgment gap, not a UI gap.

What "native ops layer" actually means

Inside Nullify, program management is a service — internally manager, exposed to customers as Campaigns. Its job is to run the assign → track → verify → close loop for a whole org without a human doing the bookkeeping. The primitive underneath it is called an Action Item, and its defining rule is the opposite of how Jira works: in Jira or a spreadsheet, a human creates the ticket, sets the priority, decides who it goes to, and clicks done. In our model, a Nullify agent creates the item — because it hit a wall it genuinely can't clear on its own — routes it, does the state-changing work, and closes it. The human supplies exactly one missing piece of judgment: an answer, a name, a decision, in one sentence of chat. There is no human-facing "create ticket" or "mark done" endpoint at all.

That inversion is the whole point at the concept level: the system doesn't ask you to do bookkeeping, it asks you the one question it can't answer for itself, and then it does the bookkeeping. Each item corresponds to a specific judgment gap — an escalated finding to decide on, a clarifying question blocking a low-confidence triage, an unknown GitHub-author-to-Slack identity, an unowned repo — rather than a generic "task," so answering one question can fan out to every finding it touches. Notice what the last two of those replace: the spreadsheet held by one person mapping GitHub handles to Slack, and the ownership data nobody was maintaining. Those stop being files and become resolvable questions the system raises exactly when it needs the answer, and never again once it has it.

The detailed mechanics of that loop — the four typed item kinds, how each surface resolves an item, and how completion is gated on the side effect actually succeeding — are the subject of a companion post, Action Items, Not Alerts. This post stays at the altitude of why a program needs this layer at all.

Judgment, not a queue: why this person, why this priority

The heart of the differentiation is the escalation itself. When the ops layer routes a finding to a developer, it doesn't drop a ticket in a queue with a name attached. Every escalation carries a structured, generated justification for why this human:

  • Ownership reason — "you were the last person to modify the vulnerable code path." Derived from actual git and code context, not a round-robin rotation or an on-call spreadsheet.
  • Business impact — "expose PHI and create HIPAA compliance risk."
  • Blocker — "CI is passing and approval is the only blocker."
  • Requested action — the exact unblock step.
  • SLA — rendered from policy ("this is a <severity> risk with a <duration> SLA"), not typed into a cell.

Crucially, the facts — finding type, repo, file, dependency version, assignee name, campaign, fix-PR URL — are resolved server-side from the system of record. The agent doesn't retype them, which eliminates the transcription-error surface that every spreadsheet carries. The agent supplies only the judgment: severity, category, ownership reasoning, business impact. The message is then assembled deterministically from those structured fields, so if the wording ever looks off, it's the template — not a model hallucinating a status.

That "why this person" reasoning isn't decoration. It's what makes the loop auditable and overridable at the same time. A human reviewer can read the ownership reason and disagree — and re-route the item to a different team — rather than silently inheriting a routing decision they can't see the basis for.

How does the system know who owns code in the first place? Through a continuously-synced ownership index, resolved by a cascade of fallbacks rather than a manual assignment: CODEOWNERS for the file path, the team that owns the repo, git blame on the affected lines, the most active committer in the directory, and so on down. This is where the payoff is most measurable. When we re-ran ownership resolution across a set of production tenants, the share of unmapped findings dropped dramatically — in one case a customer's backlog went from more than 90% unowned to roughly 35% unowned, purely by consuming CODEOWNERS and team data the org already had, sitting unused. The ownership answer was never missing. It was just never being read. A spreadsheet can't read it; an ops layer can.

"Closed" has to mean closed

The single most important property of a trustworthy ops layer is that it can't lie to you about what's done. This is precisely where spreadsheets fail: the "closed" checkbox is a human assertion, decoupled from reality.

Our resolution model is fail-loud. An item reaches completed only after both the item write and its real-world side effect succeed; if the side effect fails, the item stays open. There is no "completed-with-failed-side-effect" state — every item is either open (still needs action) or terminal (action verified). And the system watches the underlying state continuously: if a finding gets fixed through some other path, or a repo is deleted, or a user gets mapped elsewhere, the item auto-resolves because its need genuinely evaporated. But it will never auto-close an item just because nobody answered — silence is not resolution. A spreadsheet row goes stale and no one notices; here the system both refuses to fake a resolution and proactively clears the ones whose need is really gone. (The companion post walks through exactly how that gate is enforced per item kind.)

Efficiency is the point, not a side effect

None of this matters if the ops layer just generates more noise than a human ever could. So the design is aggressively biased toward doing the least work — human and machine — to get the outcome.

  • Dedup by deterministic key, not prose. An LLM phrases the same underlying blocker a dozen different ways across runs. A naive ticket-per-mention approach would flood the queue and burn tokens re-processing duplicates. Instead the dedup key is computed deterministically — the item kind plus the sorted finding IDs, never the wording — so every rephrasing of the same blocker over the same findings collapses to a single row via a partial unique index.
  • No pestering. Creation-time escalations go out as they happen — when a finding needs a decision now, the message is sent then, not held for a batch. It's the follow-up pass that's batched: the re-nudge for aging items is collapsed into a single digest per team channel per run rather than one message per finding. There's at most one re-nudge per aging item, ever, and when dedup appends new findings to an existing item nothing re-notifies and no badge re-increments. A dismissed escalation stays quiet for seven days before a still-live finding may re-escalate — long enough not to nag, short enough that a genuinely live vulnerability isn't silenced forever.
  • A hard ceiling on new work. The backlog-selection agent that assembles each campaign run operates under caps that aren't just prompt guidance but are enforced in code: at most five new PRs across all repositories in a single run, and no more than three open Nullify PRs in any one repository. The propose-PR path returns a rejection error the moment a limit is hit, so even when the backlog would justify more, the system can't flood a team with churn. It also curates a large candidate backlog down to a small working set per run and can defer actions to a future run, pacing remediation rather than firing everything the instant it's found.

The point of all of this is a program that produces outcomes — findings found, triaged, tracked, and fixed — for the least human time and the least token spend. That is the headcount decoupling made concrete: outcomes scale with context and compute, not with how many people you can put on the problem.

Optional control, full visibility

Autonomy that you can't steer is its own kind of black box, so the loop is built to stay open to a human without depending on one.

The dashboard is always the system of record. Most orgs don't wire up a messaging platform to their security tooling, so the entire feature is fully usable from the operator UI alone. A Slack or Teams thread is an optional mirror, never a load-bearing path — the feature is never silently disabled because a channel is missing. When no channel exists, an item is explicitly counted as "unrouted" in the summary rather than quietly dropped.

Where a human can reach in, the intervention points are deliberate and narrow:

  • Reopen a dismissed item — the sole human-driven terminal transition. A human can veto a dismissal, but there's no path to fabricate a "done."
  • Re-route an item to a different team. Ownership is yours to change; status is not something you set directly.
  • Answer in chat — any tenant user can supply the missing judgment; visibility is the only gate.

None of this assumes the automation is always right — it won't be. An ownership cascade can point at the wrong team when CODEOWNERS is stale; a generated justification can misread why a finding matters; a priority can land too high or too low. The design's answer isn't to claim it never happens, it's to make every one of those failures cheap to catch and reverse: the "why this person" reasoning is shown so a wrong routing decision is visible rather than silent, re-route and reopen exist precisely so a human can correct it, and the fail-loud model means the worst case is an item that stays open one beat too long — never one that reports a fix that didn't happen. A mis-routed item is an annoyance you can see and fix; a fabricated "done" is the failure that actually erodes trust, and that's the one the system is built to make impossible.

And everything the system does is measurable. A summary endpoint reports counts by status, kind, and team, plus the unrouted count, powering per-team unread badges. Every escalation is recorded as an event with success or error status, independent of whether the notification actually delivered — so "the escalation isn't in Slack" is never the same as "the escalation was lost." Every action the ops layer takes against a finding — a fix PR, a ticket, a message, an escalation — emits its own auditable event. There is no manual status update someone forgot to make, because there are no manual status updates.

That measurability is also what lets a leader trust the automation enough to hand it real work — but there's a precondition underneath all of it that no dashboard can supply. None of this control matters if the rows themselves aren't believable.

The precondition: trust the row

We built all of this on top of validated, triaged findings rather than raw scanner output for a reason. A tracked item is only as good as whether people believe it's real. One customer told us they'd evaluated a competitor that was genuinely good, but the constant battle over false positives and triage was the deal-breaker — a battle they didn't have with us. Their lead developer initially waved findings off as false positives, until he saw a proof of exploit and said, in effect, okay, I'm going to fix these.

That's the whole chain. A spreadsheet row nobody trusts is a row nobody acts on. Automation nobody trusts is automation everyone routes around — and we've seen exactly that, developers hand-fixing findings outside the flow because the system's judgment hadn't earned their confidence yet. The ops layer only scales the loop when the loop is trustworthy end to end: the finding is proven, the priority is right, the owner is correctly identified, and "closed" is verified rather than asserted.

Program management for security has been stuck as clerical work — assign, track, chase, update, repeat — pasted across a ticket board, a chat tool, and a spreadsheet nobody can quite trust. The way out isn't a better spreadsheet. It's moving the judgment into the system: let context decide who owns the code and what the real risk is, let the agents do the assigning and tracking and verifying and closing, and keep a human in the loop for exactly the decisions that need one. That's how a program's outcomes stop scaling with its headcount — which, with attackers scaling on cheap compute and no headcount at all, is no longer a nice-to-have.

Book a demo