-
-
Privacy Guard: a deterministic engine traces personal data through each merge request; GitLab Duo flows fix and verify.
-
MR !1: privacy-review ran the engine in its sandbox, found 2 blocking flows, pushed fix d9de1249 and verified it with a second run.
-
Incident #9 from privacy-watch: synthetic IDs in production logs at release bada7ab5, site 5c59002eae11, and LOG_LEVEL drift.
-
Inline note on the sink line with the source-to-sink path, then the flow's reply with before and after engine results.
-
Flow session on MR !6: install the engine from bundled wheels, analyze, edit, re-run, commit as privacy-review, push.
-
The same session's report: a hashed national ID is still personal data. Blocking 1 before the fix, 0 after.
-
Session for MR !1, started by a mention: the agent's own account of the fix and why it was allowed to commit.
-
Agent sessions in the project: reviews, verified fixes and evidence packs.
-
evidence-clerk's pack on MR !1: data touched, findings, resolution and verification, with its evidence limits stated.
-
privacy-debt issues the flow opened for info findings it did not fix.
-
Pipeline on main: tests, privacy job, benchmark, SAST, build, code map, evidence, staging deploy, Pages. Production waits for a person.
Inspiration
A reviewer approves a one-line change: logger.info({ applicant }) in a scoring route. The national ID inside applicant arrived from another file, through two function calls, inside an object. From the next deploy on, every ID that passes through lands in production logs that anyone with log access can read.
Teams that build lending, health or HR software find flows like this late: in a log search, an audit or an incident. Then they have to work out what was exposed, since when, from which release, and whether a regulator must hear about it. GDPR Art. 33 allows 72 hours for that notice where one is required.
SAST tools know injection, not personal data. LLM reviewers read the diff, but they can give a different answer on each run and cannot show that a fix worked. We wanted a deterministic check that knows which fields are personal data, agents that do the slow part (fixing, verifying, writing it up), and a person who approves the outcome.
Who has this problem
Any team that ships services handling personal data: lenders, clinics, HR and payroll tools, marketplaces. Three people feel it: the engineer who adds a log line, the reviewer who cannot see where the value came from, and the person who answers to the regulator.
It happens to well-run teams:
- In May 2018, Twitter and GitHub each found that a bug had written user passwords into internal logs in plain text.
- On 27 Sept 2024, the Irish Data Protection Commission fined Meta €91 million, with a reprimand, for passwords stored in plain text on internal systems. It found infringements of GDPR Art. 5(1)(f), 32(1), 33(1) and 33(5).
The stakes are written into the law: infringing the Art. 5 principles can cost up to €20 million or 4% of worldwide annual turnover, whichever is higher (Art. 83(5)).
What it changes, measured on our runs
- Before merge. On MR !1 the flow went from a mention to a pushed, engine-verified fix in about one minute (17:27:22 to 17:28:25 UTC) and finished its review notes and replies a minute later. The blocking check itself is deterministic: about 0.05 s on the benchmark, no LLM calls, no GitLab Credits.
- In production. Five synthetic applications under the debug setting became Incident #9 96 seconds after the first ID was logged. We started the watcher by hand; on a schedule it reads a 15-minute window.
- Response. From the hand-off (18:56:03) to a root-cause note at the exact release and line, a breach assessment draft, a notice draft and draft fix MR !7: 6 minutes. privacy-review then checked !7, engine plus 22 app tests in the sandbox, in 2 minutes.
- Adoption. One
include:line and apolicy/folder that the project site generates from a few answers. Laws plug in as policy packs, so the same engine serves other regimes.
What it does
Privacy Guard adds two supervised workflows to a GitLab project.
On every merge request
- The
privacyCI job runs the ctxgraph engine on the merge base and on the head, and reports the personal-data flows the change introduced or changed. Blocking findings fail the job. So do parse errors, invalid policy, and any analysis of changed code the engine could not finish: it fails closed. - The privacy-review flow on the GitLab Duo Agent Platform installs the engine inside its own sandbox, re-runs the analysis and submits one review, with an inline note per finding: the field and its category, the source-to-sink path as
file:linehops, what it may engage (for example GDPR Art. 32 or Art. 5(1)(c)) and a fix. - For each blocking finding it commits the smallest fix, runs the engine and the app tests again, and pushes only if every targeted finding id is gone, nothing new appeared and the tests pass. It replies on the thread with the commit SHA and the before and after results, then resolves it. Warnings become
privacy-debtissues. If a person has already approved the MR, it only suggests, because a commit would reset that approval. - When a person approves, evidence-clerk posts an evidence pack (findings, fixes, tests, approvals) as a note, without touching the approved code. A person merges.
After the merge
- Each build stamps its commit SHA into the container image and every log line, and stores that release's code map (logger site to file, line and function) and its evidence in the GitLab package registry.
maindeploys to a staging service on Google Cloud Run. Production waits for a person behind a manual job, which also creates the GitLab Release. - privacy-watch scans Cloud Logging for structured identifiers and compares the live Cloud Run configuration with
deploy/service.yaml. A match opens an Incident. When a person hands it over, the privacy-incident flow resolves the log site against the code map of the release that was running, looks for the introducing change, checks whethermainstill has the sink, opens a draft fix MR and writes a breach assessment draft. Whether anyone is notified stays a human decision. Incident #9 below is a real run.
How we built it
- ctxgraph: Python 3.12 and the tree-sitter TypeScript grammar, which also reads JavaScript; about 2,500 lines, no LLM calls. It builds a field-sensitive data-flow graph of the whole service and follows assignments, destructuring, object fields, spread, branches, loops to a fixed point, return values, template strings, and calls across files through ES imports and CommonJS
require. It finds handlers declared as functions, written inline on any router object, or wrapped in helpers such ascatchAsync. Hashing and masking keep a value tracked as pseudonymized or partially masked, and derived values such as a credit score stay personal data. Only redaction, removal or an allowlisted subset ends a flow. - Policy as YAML: data categories and tiers, sinks (logs, responses, URLs, storage, third-party hosts with their regions), transforms, guards and declared purposes. Laws plug in as packs; we ship one for the EU GDPR. Each finding gets a stable id, a severity and the articles it may engage, and each run writes a Mermaid diagram of the flows. 86 engine tests, written before each rule.
- Three custom flows in
flows/, each with its own service account, so every agent commit shows the flow as its author. Triggers: mention or reviewer assignment for privacy-review, MR approval or mention for evidence-clerk, Incident assignment or mention for privacy-incident. - Everything runs in the flow sandbox: flow sessions run on GitLab-hosted runners that can only reach gitlab.com, so
pipandnpmcannot install anything, and the sandbox's Node.js 20 is too old for the app. We vendor the engine's Linux wheels inctxgraph/wheels/. A CI job publishes the app'snode_modulesand a Node.js 22 runtime to the project's package registry, keyed by the lockfile. The flows install both offline and run the engine and the app tests before they push. Theengine-offlineandapp-offlinejobs run those exact commands on every pipeline, the second in the sandbox's own image. - CI/CD: 20 jobs on
mainacross test, build, deploy and release: app and engine tests, the privacy job, the benchmark, the field test on four open-source apps, GitLab SAST and secret detection, a CycloneDX SBOM, the offline checks for the flow sandbox, the image build, code map and evidence uploads, Cloud Run deploys, GitLab Pages and the gated release. - Google Cloud: Cloud Run for the demo app and two stub third parties (KYC in europe-west1, analytics in us-central1, so transfer findings point at a real region), Artifact Registry and Cloud Logging. The deployment code is in
deploy/andci/deploy.yml. - Demo app: a small lending service in TypeScript (Express, SQLite, pino) with synthetic data only.
- Adoption: another project adds one
include:and apolicy/folder. The project site on GitLab Pages generates both from a few answers and shows the latest benchmark run.
Benchmark
32 TypeScript cases (25 with expected findings, 7 benign), frozen at commit 0a73933 before the engine's propagation rules were written. The benchmark job runs all three tools on every push to main. Latest run, job 17479290270 on commit a9efb52, 26 Oct 2026:
- ctxgraph: recall 100% (24 of 24 in scope), precision 100%, severity agreement 100%, 0.05 s
- Semgrep Community Edition 1.182.1, taint mode: recall 83%, precision 83%, 2.24 s. Three of its four misses cross files, which the free edition does not track.
- Keyword scanner: recall 42%, precision 71%, 0.05 s
All three miss both out-of-scope cases (a callback, and a callee that mutates its argument). The README lists what the tracer does and does not follow.
Beyond the demo app: four real codebases
The engine reads TypeScript and JavaScript, ES modules and CommonJS. A field-test CI job runs it on four popular MIT-licensed Express apps at pinned commits, with a policy of common field names (email, name, phone, password and so on). Job 17484358596, 27 Oct 2026:
- 136 files across four apps (bulletproof-nodejs 5.8k stars, express-typescript-boilerplate 3.4k, node-express-boilerplate 7.7k, hackathon-starter 35k), each analyzed in under half a second, with no parse errors and no incomplete analysis.
- Three real potential issues in bulletproof-nodejs: its sign-up, sign-in and sign-out routes log the whole request body at debug level, email and password included on sign-up and sign-in. Its default log level is
silly, so those lines run unlessLOG_LEVELis set. It is the pattern behind Incident #9. - Seven response lines that need review: they return a user object built from the request, and both apps remove the password in code the tracer does not follow (a
delete, a mongoosetoJSONhook). We list them as over-reports rather than hide them. - What it cannot see yet: express-typescript-boilerplate uses decorator-based controllers, which the tracer does not follow.
The demo merge requests
Six MRs show the flow at work, each with its review, commits and pipelines:
- !1 logs the scoring result. The flow logged operational metadata instead (
d9de1249); its two blocking findings are gone. Merged. - !2 sends an analytics event on apply. The flow dropped the email from the event (
bceb4f70). Merged. - !3 reports uptime. A benign control: the privacy job passes.
- !4 redirects to
returnTo. The flow removed the applicant email from the redirect URL (9c68444d). Still open: the redirect target is not validated yet. - !5 adds a lookup by id for support staff. The flow added an owner check (
0cc029e4), which took blocking findings from 2 to 0, asked a person to confirm the support role, and openedprivacy-debtissues for the fields in the response. - !6 sends a hashed national ID to analytics. The flow removed it (
dbb82a03): a hash of an ID is still personal data. Merged.
A real incident, end to end
On 26 Oct we ran the production path with synthetic data:
- We started the manual
deploy-prodjob. It deployedprivacy-guard-prodon Cloud Run and created GitLab Releaserelease-bada7ab5, with links to its evidence and its code map. - Then we made the classic mistake on purpose: set
LOG_LEVEL=debugon the production service in the Cloud Run console, as an operator might during an outage, and sent five synthetic loan applications. - privacy-watch found 15 potential personal-data matches in Cloud Logging (national ID, email and phone, shown only as the last 4 characters) at release
bada7ab5, logger site5c59002eae11, andLOG_LEVELdrifted fromdeploy/service.yaml. It opened Incident #9. - A person handed the incident to the privacy-incident flow. In its sandbox the flow installed the engine from the wheels, resolved the site against release
bada7ab5toapp/src/debug/debugBodyLogger.ts:7, traced the national ID, email and phone into it, and found the commit that added the logger and theprivacy-okwaiver whose assumption (debug is off in production) had broken. - It posted a root-cause note, a breach assessment draft that leaves the decision to people, and a notice draft marked as needing legal review, and opened draft MR !7, which logs route and status instead of the request body.
- privacy-review then checked !7 in its own sandbox: engine exit 0 with no introduced findings, and the app tests passed, 22 of 22. A person set
LOG_LEVELback toinfo; the flows never change the live service.
Challenges we ran into
- No network in the flow sandbox beyond gitlab.com, and a Node.js too old for the app. The first flow run could not install the engine, and later runs could not run the app tests. Bundled wheels fixed the engine; a Node.js 22 runtime and the app's
node_modulesin the project's package registry fixed the tests. Two CI jobs keep both paths working, one in the sandbox's own image. - A flow's service account cannot be assigned to an Incident. GitLab refuses it, so a person starts the incident flow with a mention.
- Approvals that are not approvals. The approvals API reports
approved: truefor an MR that needs no approvals, so the first flow version refused to commit. It now checks for an actual approver. - Flow validation rejects HTML comments in prompts, so the evidence pack's marker became a heading.
- Look-alike fixes. Hashing, masking and keeping a derived score look like fixes and are not. The engine keeps tracking them and the flow's fix rules forbid them.
Accomplishments that we're proud of
- Five agent-authored fix commits, each checked by re-running the engine inside the flow's sandbox before the push. Three are merged.
- A production incident traced to its code: from the watcher's Incident #9 the incident flow found the exact logger line in release
bada7ab5, the commit that added it and the waiver that broke, and its draft fix passed review and 22 of 22 app tests inside the sandbox. - 100% recall and precision on a benchmark frozen before the rules existed, measured in CI next to Semgrep CE.
- On four real open-source apps it found the same debug body logging as Incident #9 in a template with 5.8k stars, and it lists its over-reports in the open.
- All nine lifecycle stages do real work, from
privacy-debtissues in Plan to the per-release code map that links a production log line back to its source in Monitor.
What we learned
Agents write the change and explain it well; a deterministic check is what makes the change trustworthy. The flow calls a fix verified only when the engine and the tests agree. And a sandboxed agent can only use what it can reach on gitlab.com, so the engine ships with its wheels and the app's Node.js and dependencies come from the project's package registry.
What's next
- Callbacks, class fields, decorator-based controllers and deeper object nesting in the tracer, then Python and Go.
- More policy packs, starting with India's DPDP Act.
- The incident path on a pipeline schedule for every project that adopts it.
Links
- Code (MIT): https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43514011/showcase
- Site and set-up generator: https://showcase-f8239d.gitlab.io/
- Service status (demo app on Google Cloud Run): https://privacy-guard-staging-rd2ifr3vxa-ew.a.run.app/health
- Production incident: https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43514011/showcase/-/issues/9
All personal data in the project is synthetic. Findings are potential issues for human review; the project does not certify legal compliance.
Built With
- cyclonedx
- docker
- express.js
- gitlab
- gitlab-ci
- gitlab-duo-agent-platform
- gitlab-pages
- google-artifact-registry
- google-cloud-logging
- google-cloud-run
- javascript
- mermaid
- node.js
- pino
- python
- semgrep
- sqlite
- tree-sitter
- typescript
- yaml
Log in or sign up for Devpost to join the conversation.