AI copywriting copilot with confidentiality gates
Built the LinkedIn content copilot that ships weekly drafts from user work – with a two-sided confidentiality gate that stops client pipeline data from ever entering the prompt, not just from leaving it.
My contribution
Shaped the copilot through direct product use, investigated the confidentiality incident and translated it into product and engineering requirements. Designed and implemented input and output checks, added regression tests, and carried the changes into production and ongoing use.
Open full-size image ↗
Situation
Saga is a LinkedIn content copilot: it drafts weekly posts from the user's own work, schedules them, and lets a user post to their personal profile or company Pages. It ships as a paid product on top of a live LinkedIn OAuth integration.
Three pressures compounded through summer 2026:
- LinkedIn dual-app auth was quietly rejecting valid grants. LinkedIn returns granted scopes as comma-delimited in the token response but space-delimited per the OAuth2 spec. A split-on-whitespace parser made the gate silently deny users who had actually granted the right permissions.
- Weekly per-user drafts on Vercel cron didn't scale. Vercel cron is a single global schedule; per-user weekly drafting needed per-user schedules with retry budgets. The right primitive was Trigger.dev v4.
- A vision-pass feature that read the user's own product dashboard turned into a confidentiality bomb. A round-6 draft for one client shipped with three named drilling contractors, one Norwegian operator, and their exact pipeline signal counts. The fabrication checks all passed. The facts were real. They just weren't ours to publish – they belonged to the client whose dashboard the model had read.
Task
Decide how to close the confidentiality hole permanently – not by tightening the leak-out check that had passed the offending draft, but by designing a gate that assumes the model cannot be trusted with material it should not have.
Action
System workflow
Architecture
Two-sided confidentiality gate. The old gate (detectConfidentialLeak) ran on the draft on the way out. The new gate – shipped as PR #345 – runs the same class of check on the grounding facts on the way in. The prompt never sees the confidential material in the first place, so the model can't reveal it even if the leak-out check has a blind spot.
Metric classification via regex. The CONFIDENTIAL_METRIC_RE regex catches metrics whose labels or values contain pipeline vocabulary (signals, leads, prospects, deals, tracked, briefing, targets, accounts, customers) and holds them out of the grounding block. Outcome metrics ("5+ hours saved") flow through unchanged. The regex is broad enough to catch the failure that happened and narrow enough not to censor legitimate outcome data, which was measured on the two real work items that had already been drafted.
Protected-client-name heuristic. Vision-pass reads of client dashboards used to lift UI vocabulary as proper nouns – "Results", "All", "Download PDF" – which the leak-out check then rejected as unfamiliar company names, blocking clean drafts. protectedClientNames() filters generic UI vocabulary, calendar tokens ("Aug", weekday "Monday" but not "Monday.com"), and sector labels, while preserving real entity names. On the two real work items, false positives dropped to zero without weakening the block on real client names.
Model stratification by use case. Opus 4.6 for the interactive chat (user-facing quality), Sonnet 4.6 for batch drafting (throughput), Haiku 4.5 for cheap non-writing judgments – scene inference, image verification, same-person confirmation. Haiku is used deliberately for checks where the alternative is no check at all, with the reasoning captured in the file's header comment.
Alternatives considered and rejected:
- Rebuild the leak-out check with a stronger model. Would have improved recall on the same class of failure but not addressed the root cause: material the model shouldn't have was flowing into the prompt.
- Skip vision-pass entirely. The vision-pass is the source of most of the quality lift on real work items. Removing it removes the product's edge.
- Client-supplied allowlists. Puts the burden on clients to enumerate every confidential noun in their own business – this fails the moment a client onboards a new prospect.
Implementation
- Migrated per-user cron from Vercel to Trigger.dev v4. Weekly
schedules.taskper user with anexternalIdofuserId:postingContext, three-retry budget per draft, exact-time firing. Vercel cron's global scheduler couldn't express any of that. - LinkedIn scope parsing tolerant of both delimiters.
/[\s,]+/splits on either whitespace or comma sograntIncludesOrgScopesworks regardless of which LinkedIn representation the day's OAuth response ships with. - Langfuse instrumentation on every Claude call. Traces are tagged with the resolved user ID (
resolveTraceUserId); the Langfuse instance is explicitly flushed and nulled on function teardown to avoid losing telemetry when Vercel terminates the serverless function mid-request. - Cost tracking at the API layer. Every Claude call is tagged with its model in a shared resolver so spend can be totalled by route and context, with Opus/Sonnet input/output cost ratios hardcoded as constants.
Verification
- 208 test files, 3,485+ assertions. Confidentiality tests use the actual leaking-draft example and verify both directions: real prospect names in ingested metrics are caught before the prompt, and the same names never appear in generated drafts.
- Three evaluation suites. Brand adherence, draft quality, framing A/B, and avatar likeness. Avatar likeness runs against real Gemini and Replicate vision, gated on an explicit env var (
AVATAR_LIKENESS_EVAL=1) – the eval costs $4 and takes 20–30 minutes per run, so it never fires accidentally. - 80% minimum unit + integration + E2E coverage. House rule, enforced.
Result
Open full-size image ↗
- The confidentiality incident that motivated PR #345 has not recurred. The two-sided gate holds on both known-leak inputs (verified in tests) and real production drafts since.
- Weekly per-user drafts ship reliably on Trigger.dev v4 with a retry budget rather than accumulating silent failures under Vercel cron.
- LinkedIn org posting works for users who granted the right scopes, regardless of which delimiter LinkedIn's token response uses this week.
- Model-per-purpose cost split keeps chat feeling premium while batch drafts stay affordable – Opus for user-facing prose, Sonnet for scheduled work, Haiku for verification.
Second-order effects: the two-sided-gate pattern is now the default for any product that ingests vision-pass or user-supplied context. The rule is written down: gate on the way in and the way out; assume the model cannot be trusted with material it should not have.
What we would do differently: the confidentiality regression that triggered PR #345 should have been caught by a "does this draft look like it contains anyone else's pipeline data" check running as a pre-commit eval on every model or prompt change, not just as a unit test. A live spot-check on the last N shipped drafts would have surfaced the pattern before a client did.
Skills evidenced
- AI safety patterns in production – two-sided confidentiality gate, incident-driven design, false-positive measurement against real work items.
- Multi-model orchestration under cost discipline – Opus/Sonnet/Haiku split by use case, tagged per-call cost tracking, model choice justified in code.
- OAuth integration debugging – LinkedIn's dual-format scope response, dual-app choreography for personal-vs-org posting.
- Trigger.dev v4 for per-user scheduling – per-user
externalId, retry budgets, exact-time firing, migration path from Vercel cron. - Evaluation discipline – three eval suites, gated expensive evals, avatar-likeness runs against real vision APIs when explicitly requested.
Portable lessons
- Gate on the way in, not just on the way out. If a leak-out check has a blind spot and the material was in the prompt, the failure is inevitable. Don't put material the model shouldn't have into the model.
- Model stratification is a cost tool as much as a quality tool. Opus everywhere reads well and burns money; Haiku everywhere is unusable. The right rule is per-use-case, written down where the model is chosen.
- The check that catches the incident should catch every incident of the same shape. A regex the client-facing team can read beats a system-prompt tweak that only the author understands.
Want to discuss the decisions behind it?
Over fifteen years of consulting and growth, and hands-on AI delivery from discovery to daily operations. I can share my CV and talk through how I connect business outcomes with systems that run.
Available for a chat. In person in Stavanger, or remotely worldwide.