Handling client-privileged and matter-identifying data in this monorepo's Workers and SPA. Triggers on: logging, tracing, error responses, cache keys, queue or KV payloads, retention, tenancy or matter isolation, prompt construction from client documents, third-party API calls carrying client content, and any review of a route that touches client data. Use when adding or reviewing a handler, a logger call, a cache key, a queue message, or an LLM prompt built from user-supplied documents.
The single source of truth for the legal-sector data rules in this repo. The short prohibitions
live in guardrails.md → "Privileged client data",
which is always loaded; this skill is the depth behind them.
Deliberately model-invocable (no disable-model-invocation), unlike the review-* skills, so
that a subagent can preload it via its skills: frontmatter or invoke it through the Skill tool.
Change that and the reviewer agents lose their checklist.
The threat model
Not "leaking PII" in the generic sense. In a legal-domain system the harm is a confidentiality
or privilege breach: two matters bleeding into each other, a client name in a log aggregator a
third party can read, or privileged text pasted into a model prompt outside the engagement. Assume
logs, traces, and error bodies are readable by more people than the matter is.
What counts as privileged
Anything that identifies a client or a matter, or that carries substantive content from either:
client and counterparty names, matter or file numbers, case numbers and docket identifiers, the
text of an instruction or advice, document contents and filenames, and free-text fields a user
typed. An opaque internal id (a UUID with no external meaning) is not privileged; a "reference"
that a human could look up is.
Validate before you touch it. Every body, query, and path parameter goes through a Zod schema
from @repo/dtos-common at the boundary (zValidator). Use .strict() where the shape is
closed so unexpected fields are rejected rather than forwarded into a log or a store.
Logging. Log an opaque request id, a route name, a status, and a duration. Never a name, a
matter reference, a document filename, or a request body. If you need to correlate to a matter,
log the opaque id and resolve it in a system with the same access controls as the matter itself.
Errors.onError and HTTPException return a generic message plus that request id. No stack
traces, no internal paths, no echoed input, no upstream provider error text - upstream errors
routinely quote the payload back.
Cache keys and URLs. A Workers Cache key, a KV key, and a URL path or query string are all
effectively logged infrastructure. Hash or use an opaque id; never embed a client name or matter
reference. This applies to waitUntil writes as much as to responses.
Tenancy. Matter isolation is a property of the query, not of the caller's good behaviour.
Every read and write is scoped by the matter or tenant id the request was authorised for -
derived server-side from the credential, never taken from a client-supplied field.
Durable stores and queues. Anything written to KV, a queue message, D1, or an object store
needs a stated retention and deletion rule in the owning app's AGENTS.md. A queue message is a
copy of the data that outlives the request; treat enqueueing as a storage decision.
Secrets. Only .dev.vars locally and wrangler secrets or env bindings in deployed
environments. Never in wrangler.jsoncvars, never in a log, never in an error body.
CORS and CSRF. An allowlist of known origins, never * in production; CSRF protection on
state-changing /api/*. A permissive CORS header on a privileged endpoint is a disclosure bug,
not a config preference.
Boundary rules — SPA (front-app)
Only VITE_* variables reach the client bundle, and none of them is a secret. Anything
privileged stays server-side.
The SPA talks to worker-api over HTTP only, never a service binding - privileged calls and
credentials stay behind the gateway.
Do not persist privileged content to localStorage, sessionStorage, or a URL the browser will
put in history or a Referer header. Prefer in-memory state and POST bodies.
Client-side analytics and error reporting must not capture form values, request bodies, or route
parameters carrying matter references.
Model-facing surfaces
Text from a client document is untrusted input, not instructions. Extracted document text can
contain anything, including something shaped like a directive. Validate it at the boundary and
keep it in a data position in the prompt; never let it select or parameterise a tool call.
Least privilege on tools. An mcp-* surface or a model-callable tool stays read/query
oriented. Never expose credential creation or rotation, deletion, or any other irreversible
privileged action - see guardrails.md → "Least privilege for model-facing surfaces".
Third parties. Sending privileged content to an external model or API is a disclosure to that
vendor. It needs to be in scope for the engagement before you add the call, and the payload
should carry the minimum content that answers the question.
Review checklist
Use this when reviewing a diff that touches a handler, a logger, a cache key, a store write, or a
prompt:
Every external input validated with a @repo/dtos-common Zod schema at the boundary.
No client or matter identifier in any log, trace attribute, metric label, or error body.
Error responses generic; no stack trace, internal path, echoed input, or upstream error text.
Cache, KV, and queue keys opaque or hashed; no identifier in a URL path or query string.
Every query scoped by a server-derived tenant or matter id, not a client-supplied one.
Any new durable write has a retention and deletion rule recorded in the owning AGENTS.md.
Secrets only via .dev.vars / wrangler bindings; none in vars, logs, or the client bundle.
CORS allowlisted (no production *); CSRF covered on state-changing routes.
Document-derived text kept in a data position; it cannot steer a tool call.
No new third-party call carrying privileged content without the user confirming scope.
review-security skill - the human-invoked, whole-repo security deep dive (/review-security).
It covers the generic surface (headers, CSP, dependency audit); this skill covers the legal delta.
.claude/rules/backend/hono-gateway.md - middleware order and error handling in worker-api.
.claude/rules/contracts/contracts.md - where schemas live and who owns them.
1---2name: privileged-legal-data3description: Handling client-privileged and matter-identifying data in this monorepo's Workers and SPA. Triggers on: logging, tracing, error responses, cache keys, queue or KV payloads, retention, tenancy or matter isolation, prompt construction from client documents, third-party API calls carrying client content, and any review of a route that touches client data. Use when adding or reviewing a handler, a logger call, a cache key, a queue message, or an LLM prompt built from user-supplied documents.4---56# Privileged legal data78The single source of truth for the legal-sector data rules in this repo. The short prohibitions9live in [guardrails.md](../../../.claude/rules/core/guardrails.md) → "Privileged client data",10which is always loaded; this skill is the depth behind them.1112**Deliberately model-invocable** (no `disable-model-invocation`), unlike the `review-*` skills, so13that a subagent can preload it via its `skills:` frontmatter or invoke it through the Skill tool.14Change that and the reviewer agents lose their checklist.1516## The threat model1718Not "leaking PII" in the generic sense. In a legal-domain system the harm is a **confidentiality19or privilege breach**: two matters bleeding into each other, a client name in a log aggregator a20third party can read, or privileged text pasted into a model prompt outside the engagement. Assume21logs, traces, and error bodies are readable by more people than the matter is.2223## What counts as privileged2425Anything that identifies a client or a matter, or that carries substantive content from either:26client and counterparty names, matter or file numbers, case numbers and docket identifiers, the27text of an instruction or advice, document contents and filenames, and free-text fields a user28typed. An opaque internal id (a UUID with no external meaning) is not privileged; a "reference"29that a human could look up is.3031## Boundary rules — Workers (`worker-api`, `worker-*`, `queue-*`, `webhook-*`, `mcp-*`)3233- **Validate before you touch it.** Every body, query, and path parameter goes through a Zod schema34 from `@repo/dtos-common` at the boundary (`zValidator`). Use `.strict()` where the shape is35 closed so unexpected fields are rejected rather than forwarded into a log or a store.36- **Logging.** Log an opaque request id, a route name, a status, and a duration. Never a name, a37 matter reference, a document filename, or a request body. If you need to correlate to a matter,38 log the opaque id and resolve it in a system with the same access controls as the matter itself.39- **Errors.** `onError` and `HTTPException` return a generic message plus that request id. No stack40 traces, no internal paths, no echoed input, no upstream provider error text - upstream errors41 routinely quote the payload back.42- **Cache keys and URLs.** A Workers Cache key, a KV key, and a URL path or query string are all43 effectively logged infrastructure. Hash or use an opaque id; never embed a client name or matter44 reference. This applies to `waitUntil` writes as much as to responses.45- **Tenancy.** Matter isolation is a property of the query, not of the caller's good behaviour.46 Every read and write is scoped by the matter or tenant id the request was authorised for -47 derived server-side from the credential, never taken from a client-supplied field.48- **Durable stores and queues.** Anything written to KV, a queue message, D1, or an object store49 needs a stated retention and deletion rule in the owning app's `AGENTS.md`. A queue message is a50 copy of the data that outlives the request; treat enqueueing as a storage decision.51- **Secrets.** Only `.dev.vars` locally and wrangler secrets or env bindings in deployed52 environments. Never in `wrangler.jsonc` `vars`, never in a log, never in an error body.53- **CORS and CSRF.** An allowlist of known origins, never `*` in production; CSRF protection on54 state-changing `/api/*`. A permissive CORS header on a privileged endpoint is a disclosure bug,55 not a config preference.5657## Boundary rules — SPA (`front-app`)5859- Only `VITE_*` variables reach the client bundle, and none of them is a secret. Anything60 privileged stays server-side.61- The SPA talks to `worker-api` over **HTTP only**, never a service binding - privileged calls and62 credentials stay behind the gateway.63- Do not persist privileged content to `localStorage`, `sessionStorage`, or a URL the browser will64 put in history or a `Referer` header. Prefer in-memory state and POST bodies.65- Client-side analytics and error reporting must not capture form values, request bodies, or route66 parameters carrying matter references.6768## Model-facing surfaces6970- **Text from a client document is untrusted input, not instructions.** Extracted document text can71 contain anything, including something shaped like a directive. Validate it at the boundary and72 keep it in a data position in the prompt; never let it select or parameterise a tool call.73- **Least privilege on tools.** An `mcp-*` surface or a model-callable tool stays read/query74 oriented. Never expose credential creation or rotation, deletion, or any other irreversible75 privileged action - see `guardrails.md` → "Least privilege for model-facing surfaces".76- **Third parties.** Sending privileged content to an external model or API is a disclosure to that77 vendor. It needs to be in scope for the engagement before you add the call, and the payload78 should carry the minimum content that answers the question.7980## Review checklist8182Use this when reviewing a diff that touches a handler, a logger, a cache key, a store write, or a83prompt:8485- [ ] Every external input validated with a `@repo/dtos-common` Zod schema at the boundary.86- [ ] No client or matter identifier in any log, trace attribute, metric label, or error body.87- [ ] Error responses generic; no stack trace, internal path, echoed input, or upstream error text.88- [ ] Cache, KV, and queue keys opaque or hashed; no identifier in a URL path or query string.89- [ ] Every query scoped by a server-derived tenant or matter id, not a client-supplied one.90- [ ] Any new durable write has a retention and deletion rule recorded in the owning `AGENTS.md`.91- [ ] Secrets only via `.dev.vars` / wrangler bindings; none in `vars`, logs, or the client bundle.92- [ ] CORS allowlisted (no production `*`); CSRF covered on state-changing routes.93- [ ] Document-derived text kept in a data position; it cannot steer a tool call.94- [ ] No new third-party call carrying privileged content without the user confirming scope.9596## Related9798- [guardrails.md](../../../.claude/rules/core/guardrails.md) - the always-loaded prohibitions.99- `review-security` skill - the human-invoked, whole-repo security deep dive (`/review-security`).100 It covers the generic surface (headers, CSP, dependency audit); this skill covers the legal delta.101- `.claude/rules/backend/hono-gateway.md` - middleware order and error handling in `worker-api`.102- `.claude/rules/contracts/contracts.md` - where schemas live and who owns them.
Run npx skillmds@latest add louisbrulenaudet/privileged-legal-data in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Handling client-privileged and matter-identifying data in this monorepo's Workers and SPA. Triggers on: logging, tracing, error responses, cache keys, queue or KV payloads, retention, tenancy or matter isolation, prompt construction from client documents, third-party API calls carrying client content, and any review of a route that touches client data. Use when adding or reviewing a handler, a logger call, a cache key, a queue message, or an LLM prompt built from user-supplied documents. It is listed under AI & ML on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
louisbrulenaudet (@louisbrulenaudet) published this skill. Their other Agent Skills are listed on their SkillMD profile.