How We Used AI to Increase the Security Level of Our Code

Summary: We replaced a once-a-year external security audit with an AI-driven one we run ourselves, across 100% of our services, as frequently as we want. The core is a Claude Code skill we built in-house — within about a day it was already surfacing issues that our previous external audits had missed. We estimate it saves around $150k a year. Here’s how it works, what it found, and the areas where it does not replace a human.

Joko is a shopping companion used by more than 6 million people in France and the US. Our users connect their bank account to the app, and we operate a payment system on top of it. That makes security a first-order concern: we handle sensitive personal and financial data, so a gap in our code is not an abstract risk but a direct one to our users.

Waiting skeleton meme: waiting for next year's audit to flag the wildcard IAM role we shipped in Q1

Why bring this in-house

The default for a company our size is to pay an external firm for a security audit. Since it’s often fairly costly and requires onboarding an external security analyst, this is something that is generally only done once a year. It works, but it has three structural problems.

It’s expensive. It’s a point-in-time snapshot: by the time the report lands, the code has already moved. And it tends to be strong on generic issues (known CVEs, missing headers) but shallow on the things specific to your product, because the auditors don’t know your domain well.

We wanted the opposite: continuous coverage, across every service, that actually understands our business logic. That only became realistic with AI in the loop.

What a security audit actually is

It helps to split an audit into two halves.

The first half is mechanical scanning: known-CVE lookups, leaked-secret detection, OWASP static patterns. Tools have done this well for years. It isn’t where the money or the expertise goes.

The second half is contextual reasoning: can we forge money in the user’s wallet by going through this endpoint, does this admin handler actually check the requester’s role, is this money-movement webhook safe to replay, does this Lambda log sensitive user payment info to CloudWatch. Answering those means understanding the system, not pattern-matching. That is the part that used to require an expensive human, and it’s exactly the part a capable model now does well.

The whole design follows from that split.

Iceberg meme: mechanical scanning (CVEs, leaked secrets, OWASP patterns) is the tip above water; contextual reasoning (can you forge money in your wallet, does this admin route check the role, is this webhook replayable) is the mass below

The skill: five passes

We wrote a security-audit skill in our internal Claude Code plugin. Point it at one repo or all of them, and it runs five passes in order: three deterministic, two reasoning-based.

Pass 1 — Dependency CVEs. Most package managers ship their own tool for auditing dependency CVEs. For Node and npm, that’s npm audit --json, for example.

npm audit --json

Pass 2 — Secrets and credentials. This spans a spectrum, from basic grep commands to more advanced, dedicated tooling.

# from a basic grep...
git grep -nE "(api_key|secret|password|token|private_key|connection_string)\s*=\s*['\"][^'\"]{8,}"
# ...to more advanced tools like trufflehog

Pass 3 — OWASP static patterns. Static analysis catches a broad class of known-insecure patterns without running the code. We use semgrep with the OWASP Top Ten ruleset.

semgrep --config=p/owasp-top-ten --json .

Those three are just orchestration around existing tools. The value starts at pass four.

Pass 4 — Infrastructure and config (reasoning). The model reads your infrastructure and deployment configuration directly — IaC, CI/CD, runtime manifests, and environment configuration (e.g. Terraform/CloudFormation, Kubernetes/Helm, Docker, GitHub Actions, and app manifests like package.json, pom.xml, requirements.txt) — looking for over-permissive rights (cloud IAM or K8s RBAC), wildcard actions, missing encryption, open CORS, exposed config, and risky install hooks.

Pass 5 — Business logic (reasoning). The model reads the source for product-specific risks: forging money in the user’s wallet through an unsigned or unauthenticated payload, admin privilege escalation from missing role checks, sensitive user payment info logged to CloudWatch, and sensitive routes accepting unauthenticated calls.

The output is a severity-ranked report (Critical / High / Medium / Low), each finding with a file:line reference and a one-line “why this is dangerous”. It then deduplicates against existing tickets and can propose Linear tickets from the findings — with the option to filter to Critical and High entries only, so you stay focused on the most important points and avoid losing focus. The report turns into triaged work rather than a PDF nobody actions.

Security Audit  payment-service  2026-06-20

CRITICAL
 (none)

HIGH
 Wildcard IAM action on deploy role (serverless.yml:212)  role can act on any resource

MEDIUM
 PII (email) written to CloudWatch (src/handlers/notify.ts:88)

Tools run: npm audit  | semgrep  | trufflehog 

What it found

Naming the classes of issue it surfaced, all since triaged and closed:

  • Over-broad rights: wildcard actions on roles, scoped down to least privilege.
  • CI credentials: long-lived cloud keys in CI, migrated to short-lived OIDC.
  • Security headers: HSTS max-age too short (raised to a year) and missing anti-framing / clickjacking headers (added).
  • Observability leakage: Sentry breadcrumbs capturing full request URLs — which can embed tokens or personal data in query parameters — with no scrubbing configured.
  • Logging hygiene: PII (personally identifiable information — emails, payment details) reaching logs from a handful of handlers.

None of these are exotic. That’s the point. They are the quiet, boring gaps that accumulate between annual audits, and they’re exactly what a continuous in-house process catches early.

Hide the Pain Harold meme: when the audit flags a wildcard IAM role and git blame says it was you

From a one-shot project to a recurring habit

The work split into two phases, the same shape as a cost-cleanup.

One-shot: a full sweep across 100% of backend services and the cross-cutting concerns (secrets, IAM, CI auth, headers, logging). Confirmed findings became tickets; the P1s were pulled into a finite “close the urgent gaps” project with a hard deadline.

Recurring: the run is a skill, so repeating it is negligible for a business of our size. Findings flow into a continuous triage stream reviewed weekly, and the full sweep is now scheduled quarterly, one run per repo. Security coverage stops being an annual event and becomes routine.

The economics

A multi-week external engagement, once a year, versus a skill built in about a day and a sweep that takes a few engineer-days and runs every quarter. Adding up the audits we no longer outsource and the gaps caught before they become incidents, we estimate the in-house process saves on the order of $150k a year, while increasing coverage from once-a-year to continuous.

What we want to improve

The real limitation today is context. All our new business logic now lives in a monorepo, but we still have some legacy business logic spread across old separate repos — so for that legacy code the audit runs one repo at a time and sees each service in isolation. That’s fine for issues contained inside a service, but the most dangerous gaps live at the seams between them: a trust assumption where one service calls another, an auth check that exists upstream but is missing where it actually matters.

We know those gaps are real because we found one. The most serious issue the audit surfaced spanned several services, invisible to any single-repo run; it only came together by manually stitching separate audits into one picture. That was the strongest argument for changing how we run it.

That’s why we want to migrate all the legacy logic into the monorepo, which we already use for everything else today. Consolidating there changes what the audit can see: the model can load the whole system at once and follow a request across service boundaries instead of stopping at the edge of a repo. The skill lives in the monorepo too, versioned alongside the code it audits. More context in one place means the reasoning passes get sharper, which is where the value already is.

Takeaway

AI collapses the expensive middle of a security audit — the contextual reasoning about your own system — and turns a yearly external snapshot into a continuous in-house process. The mechanical scanning was never the hard part. The judgment was, and that’s now cheap enough to run every quarter instead of every year.