Legal Document Generator
Generate draft legal documents from company context. Supports 8 document types across US, EU/GDPR, and UK jurisdictions. All output is marked as a draft requiring professional legal review.
Supported Document Types
- Terms & Conditions
- Privacy Policy
- Cookie Policy
- GDPR Policy
- Acceptable Use Policy
- Data Processing Agreement
- Data Protection Disclosure
- Disclaimer / Limitation of Liability
Phase 0: Context Gathering
Use the AskUserQuestion tool to gather company context. Ask for:
- Company name (required)
- Product/service description (required) -- what the product does, who it serves
- Data practices -- what user data is collected, how it is processed, stored, shared
- Jurisdiction -- which legal frameworks apply (US, EU/GDPR, UK, or multiple)
- Contact information -- email and/or physical address for legal notices
If the user provides arguments after the skill name (e.g., /legal-generate privacy-policy), use that as the document type selection and skip Phase 1.
Phase 1: Document Selection
Use the AskUserQuestion tool to select a document type from the 8 supported types listed above.
Phase 2: Generation
Invoke the legal-document-generator agent via the Task tool with the company context and selected document type:
Task legal-document-generator: "Generate a [document type] for [company name].
Company: [name]
Product: [description]
Data practices: [practices]
Jurisdiction: [jurisdiction]
Contact: [contact info]"
Gates measure agreement, not truth (#7349). All five gates compare the two surfaces against each other. None asks whether the agreed text is correct, so two byte-identical copies of a false sentence pass every one of them. Three defects shipped past a full green run in #7349 that way: a controllership statement drift-reduction copied onto the published page, a duplicated clause left behind by a half-applied replacement, and "eleven processing activities" in a document whose register carries thirty-five. Read the prose; do not read the drift number and stop.
BODY_EQUIVALENCE_DOCS is a one-way ratchet. terms-and-conditions, acceptable-use-policy and disclaimer are enrolled; each was verified at ZERO normalised drift immediately before enrolment, because enrolling a drifted document turns a required check red on arrival. Once enrolled, any edit landing on one surface only reds that check — which is the point. --print-vocab and a mutation check (inject a line, confirm the guard fails, remove it) are the two ways to prove an enrolment is live rather than decorative.
Two measurement traps that cost real rounds in #7349. collapse() normalises [0-9]+ AI agents but NOT a bare [0-9]+ agents, so count divergence between the record and the published page can be invisible to the drift gate. And a grep for Article will not match the corpus's plural Articles 15 through 22 — use Articles? 1[5-9].
Phase 2.5: Redaction Gate (BLOCKING — runs BEFORE inline presentation)
A generated legal draft can echo a secret or PII that was passed in as company context (a contact email, an API identifier pasted into a data-practices answer). Presenting the draft inline in Phase 3 is a transcript write boundary — the same fail-closed rule the incident skill enforces (incident/SKILL.md Phase 6): the sentinel must precede inline-emit, not just file-commit. So the redaction gate runs here, before the operator ever sees the draft.
- Write the generated draft to a
mktempfile (do NOT emit it inline yet).
Register the cleanup trap in the same block that allocates the draft, before anything can halt. This PR adds an earlier fail-closed exit, so it increases how often the draft is abandoned mid-flight — and the abandoned file is the UN-REDACTED text, which is precisely what this gate exists to stop escaping. Leaving it in mktemp is a leak with a longer lifetime than the session (#7450 review-finding C14).
The allocation, the trap and the gate MUST share one fence — each fenced block is a separate Bash call, so a trap registered in its own block fires when that block exits, deleting the draft immediately and leaving $DRAFT empty for everything after it. The split form shipped once and all three of its guarantees were false. The combined fence is in step 2 below.
- Run the shared hardened engine against it. Resolve the path from the deployed plugin root
(${CLAUDE_PLUGIN_ROOT}, the platform-trusted copy — ADR-179's canonical bare anchor, with no fallback arm) — NOT a bare ../incident/... relative path, which depends on the current working directory and, from the wrong CWD, exits 127 outside the shim (bypassing the shim's fail-closed exit-2 normalization). On the Concierge server the deployed-root anchor is load-bearing: a bare CWD-relative path would resolve the connected repo's untrusted copy of the sentinel (ADR-093). The default arm was removed, not re-pointed: review/SKILL.md instructs gh pr checkout, after which the git worktree is the reviewed party's tree, so that arm resolved this gate's own scanner from a file a hostile PR controls (#7450). The bare anchor is the load-bearing control — the loader substitutes it with the installed root at delivery, so no ambient environment value reaches this site (measured: specs/feat-one-shot-7450-git-root-anchor-untrusted/phase-1-measurement.md Arm 4). The identity preflight below is defence-in-depth for surfaces where substitution does not govern: it is a shape check and cannot tell an install from a checkout carrying the same manifest, which is why ADR-179 §(a) measured a shape check passing while an attacker-chosen payload executed. The stronger root-outside-the-worktree form was evaluated and rejected — see specs/feat-one-shot-7450-git-root-anchor-untrusted/b1-disposition.md. Each halt emits a SOLEUR_* marker on stdout so refusals reach telemetry:
```bash DRAFT="$(mktemp)" || { echo "SOLEUR_LEGAL_GENERATE_HALT reason=draft-alloc-failed" echo "legal-generate: cannot allocate a draft file — stopping before any draft text exists." >&2 exit 2; } trap 'rm -f "$DRAFT"' EXIT INT TERM HUP
# Write the generated draft into "$DRAFT" here — in THIS fence, before the gate below. # Use a QUOTED heredoc delimiter (<<'DRAFT_EOF'): company-context answers can contain # $(…), backticks and $VAR, and an unquoted delimiter would EXECUTE the substitutions # on the operator's machine and expand $VAR to empty — mutating the text the sentinel is # about to scan, so a secret's shape can be destroyed by the shell instead of the redactor. # cat > "$DRAFT" <<'DRAFT_EOF' # <the generated draft, verbatim> # DRAFT_EOF
[ -f "${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json" ] \ && grep -q '"name"[[:space:]]:[[:space:]]"soleur"' "${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json" \ || { echo "SOLEUR_LEGAL_GENERATE_HALT reason=plugin-root-unverified root=[${CLAUDE_PLUGIN_ROOT}]" echo "legal-generate: cannot verify the Soleur plugin installation — stopping before any draft is written." >&2 echo " Resolved plugin root: [${CLAUDE_PLUGIN_ROOT}]" >&2 echo " If that is EMPTY: no Soleur plugin is loaded in this session. Install it and start a NEW session — re-running here resolves the same empty root." >&2 echo " If it names a path: that path is not a Soleur install (a repo checkout is not an install). Run 'claude plugin update soleur', then RESTART Claude Code — plugin changes apply only on restart. If you installed with --scope project or --scope local, pass the same scope. Reinstall only if that does not clear it." >&2 echo " Do NOT hand-edit and publish this draft — the redaction scanner is what makes it safe to share." >&2 exit 2;