โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ ๐ C L I P P Y โ
โ your contract analyst โ
โ โ
โ "It looks like you're signing a contract. โ
โ Would you like help checking for nasty clauses?" โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Open-source, browser-only AI contract analyzer. Multi-model. Zero storage. No server. Just truth.
Try it live โ ยท Releases ยท Report Bug ยท CHANGELOG ยท INSTALL
- What Clippy Does
- Screenshots
- Why It Exists
- The Mascot
- Features
- Quick Start
- Supported Models
- Tech Stack โ Every Choice Explained
- Architecture
- The Legal Context
- The Problem with Standard Form Contracts
- EU Law: Directive 93/13/EEC
- GDPR โ Regulation 2016/679
- US Law: Unconscionability and Arbitration
- UK Law: Consumer Rights Act 2015
- French Consumer Law
- Arbitration Clauses: The Global Picture
- IP Assignment Clauses
- Liability Caps and Indemnification
- Governing Law and Forum Selection
- The Small Print Problem
- Limitations of AI Contract Analysis
- Under the Hood
- War Stories โ Every Bug That Shaped This Project
- Project Structure
- Adding a Model
- Customizing Analysis Objectives
- Cost Estimates
- Roadmap
- Self-Hosting
- Contributing
- Security
- License
- Credits & References
- A Note on Vibe Coding and Human Leadership
Clippy lets you upload any contract โ PDF, DOCX, TXT, or Markdown โ and have multiple AI models simultaneously analyze it for risky, abusive, or deceptive clauses. It runs entirely in your browser: your contract text is sent directly from your browser to OpenRouter using your own API key. No file ever touches a Clippy server. No data is stored. No account required.
You choose which AI models to run. You choose which analysis objectives matter โ GDPR compliance? Non-compete enforceability? Hidden fees? Unlimited IP assignment? The models run in parallel. Results appear live. You can download a PDF or Markdown report, or generate a shareable URL that encodes the full results for a colleague.
For under $0.15, you get the same first-pass analysis that a paralegal would charge $150 for โ instantaneous, parallel across 8 models, with specific clause quotation and severity tagging.



Legal contracts are designed to be long, dense, and deliberately hard to parse. Lawyers are expensive and unavailable to most people at the moment they need them most โ which is when they're about to sign something. Most people sign contracts without reading them. Those who do often lack the legal background to identify what's actually dangerous vs. standard boilerplate.
Clippy bridges that gap. It doesn't replace a lawyer. It doesn't give legal advice. But it gives you, at almost zero cost, the same first-pass analysis a first-year associate would do: flag the clauses that look unusual, one-sided, or potentially abusive, and explain why in plain language.
The premise: a single paragraph of a contract signed without understanding can cost you more than Clippy's entire development. That's the product.
Clippy is named after Microsoft's Office Assistant โ the animated paperclip introduced in Microsoft Office 97. Love it or hate it, Clippy was trying to help. So is this one.
The design pays homage to the original: golden paperclip body, expressive eyes, a speech bubble. The same energy, a very different mission. The SVG is drawn entirely in code โ no external image files, no HTTP requests. Three overlapping paths create the paperclip's 3D effect: a dark gold shadow layer, a bright yellow body, and a specular highlight. The eyes blink on a randomized 3โ5 second interval so it never feels mechanical.
The name is also a small act of reclamation. Clippy was mocked into retirement in 2004. Here, it gets to be useful.
- Multi-model analysis โ Run up to 8 AI models simultaneously (Claude, GPT, Gemini, Mistral, Llama, DeepSeek)
- Parallel execution โ All selected models run concurrently via
Promise.allSettled(), not sequentially - Trust Score โ A 0โ100 score per model with animated ring visualization, color-coded by tier
- 5 analysis dimensions โ Transparency, Balance, Legal Compliance, Financial Risk, Exit Freedom โ each scored 0โ100
- Severity-flagged clauses โ CRITICAL / SUSPECT / MINOR with title, description, and verbatim quote from the contract
- 10 curated objectives across 5 categories: General (3), Financial (2), Privacy (2), Employment (2), IP (1)
- All legally grounded โ each prompt cites specific EU Directives, GDPR Articles, US case law, UK Acts
- Toggle / edit / add โ switch any objective on or off; edit the instructions; add custom objectives
- Custom prompt creation โ "Add custom objective" creates a blank prompt in your session
- Zero backend โ no server, no database, no account, no logs
- Files never leave your browser โ contract text is extracted client-side; only the text (not the file) goes to OpenRouter
- AES-GCM API key encryption โ key is encrypted in browser memory (Web Crypto API) as soon as you lock it; decrypted just-in-time before each call
- No analytics, no tracking, no cookies
- PDF export โ jsPDF text-based report with cover page, trust score badges, dimension bars, flagged clause cards
- Markdown export โ GitHub-compatible
.mdwith ASCII progress bars and blockquoted clause quotes - Share URL โ full results encoded as base64 in the URL fragment; no server needed for sharing
| Format | Parser | Notes |
|---|---|---|
.pdf |
pdfjs-dist v5 | Machine-readable only. Password-protected PDFs not supported. Scanned (image-only) PDFs return empty text. |
.docx |
mammoth.js | Word documents. Preserves paragraph structure. |
.txt |
Native File.text() |
Plain text. |
.md |
Native File.text() |
Markdown. Read as plain text. |
- 20 languages โ EN, FR, ES, PT, DE, NL, IT, ZH, RU, HI, BG, PL, DA, JA, KO, HE, AR, TR, SV, ID
- RTL support โ Arabic and Hebrew render right-to-left with correct
dir="rtl"on<html> - Locale detection priority โ
?lang=XXURL param โlocalStorageโ browser language โ English fallback - Locale-aware AI output โ analysis results delivered in the active UI language
- Demo modal โ "See Demo" hero slideshow translated in all 20 languages
- Node.js 18+ / npm 9+
- An OpenRouter API key (free to create; you pay per API call)
git clone https://github.com/paulfxyz/clippy.git
cd clippy
npm install
npm run dev
# โ http://localhost:5000npm run build
# Output: dist/public/ โ fully self-contained static siteSee INSTALL.md for deployment guides: FTP, Cloudflare Pages, Vercel, Netlify, GitHub Pages, Docker.
Add ?lang=XX to the URL:
https://clippy.legal?lang=fr โ Franรงais
https://clippy.legal?lang=tr โ Tรผrkรงe
https://clippy.legal?lang=ar โ ุงูุนุฑุจูุฉ (RTL)
https://clippy.legal?lang=id โ Bahasa Indonesia
Current model list (v3.2.5), via OpenRouter:
| Model | Provider | Default | JSON Mode | Notes |
|---|---|---|---|---|
anthropic/claude-3.7-sonnet |
Anthropic | โ #1 | Prompt-only | Fast, excellent reasoning |
anthropic/claude-3.5-haiku |
Anthropic | โ | Prompt-only | Cheapest Anthropic option |
openai/gpt-4.1 |
OpenAI | โ #2 | response_format |
Strong JSON compliance |
openai/gpt-4.1-mini |
OpenAI | โ | response_format |
Good value |
google/gemini-2.5-pro |
โ | Prompt-only | 1M context window | |
mistralai/mistral-large-2512 |
Mistral | โ | Prompt-only | Strong EU legal context |
meta-llama/llama-3.3-70b-instruct |
Meta | โ | Prompt-only | Open weights |
deepseek/deepseek-r1 |
DeepSeek | โ | Prompt-only | Chain-of-thought reasoning |
JSON mode: OpenAI models receive response_format: { type: "json_object" }. All other models receive a prompt instruction to output valid JSON. This is gated on a JSON_MODE_MODELS prefix whitelist in openrouter.ts โ see War Stories #3 for why.
Model IDs go stale. OpenRouter renames IDs as providers release new versions. The IDs above were verified against the live OpenRouter models endpoint in April 2026. If a model returns "No endpoints found," check the live endpoint and update AVAILABLE_MODELS in openrouter.ts.
React for the component-heavy wizard UI. TypeScript catches entire classes of bugs at compile time โ especially important when passing complex nested objects (contract analysis results) through many component layers. Vite is fast enough that the feedback loop never gets in the way.
Content-hashed JS/CSS bundles mean CDN caches are invalidated automatically on each deploy. The one file without a hash is index.html โ which caused a painful caching bug (see War Stories #5).
shadcn/ui gives you accessible Radix UI primitives pre-wired with Tailwind. It's not a dependency you install โ it's code you own, which means you can change anything without fighting a theming system.
The design uses a warm cream palette (#F5EDD6, #F5D000) instead of cold greys. Legal documents are stressful; the UI should feel like paper, not a dashboard.
Clippy needs client-side routing for /share/:payload but deploys as a static file. Path-based routing (/share/...) returns 404 from nginx without server configuration. Hash-based routing (/#/share/...) works on any server because the fragment is never sent to the server. wouter is 1.5KB and covers exactly this use case.
pdf.js by Mozilla is the gold standard for client-side PDF parsing. Version 5 moved to ES modules (.mjs workers), which introduced a serious deployment problem on SiteGround nginx (see War Stories #1). The fix: fetch the worker, re-wrap as a text/javascript Blob, register via GlobalWorkerOptions.workerSrc.
Extracts plain text from .docx files with paragraph structure preserved. No native dependencies. For Clippy's use case โ read the text, don't render the formatting โ it's perfect.
No Redux. No Zustand. No Context for global state. Clippy has one page and a linear wizard. The entire app state fits in a single AppState object managed by one useState hook. Keeping state co-located in Home.tsx makes resets trivial (one setState call), debugging straightforward (one place to look), and eliminates the boilerplate tax of global state libraries.
The OpenRouter API key is sensitive. Clippy uses window.crypto.subtle to encrypt it with AES-GCM as soon as the user locks it. A random 12-byte IV is generated per encryption. The CryptoKey never leaves the JS heap โ it's destroyed on tab close. The encrypted blob is stored in state; the raw key is cleared.
This is defence-in-depth against trivial key extraction โ not against a capable browser-level adversary โ and we're honest about that in the docs.
One API key, one endpoint (https://openrouter.ai/api/v1/chat/completions), 100+ models. All calls go directly from the browser to OpenRouter โ no Clippy proxy. This keeps the architecture zero-backend.
For PDF export, jsPDF's text/table API is used rather than html2canvas:
- Screenshots capture screen-resolution artifacts (CSS shadows, pixel offsets)
- jsPDF text is searchable, copyable, and accessible
- Smaller file size (50โ200KB vs 1โ5MB for screenshots)
- Works offline โ no browser paint cycle needed
btoa(encodeURIComponent(JSON.stringify(payload))) in the URL fragment. No server required. The fragment is never sent to any server โ browsers strip it from HTTP requests. The recipient's browser decodes it locally. API key never included.
Production at clippy.legal runs on SiteGround shared hosting, deployed via Python ftplib.FTP_TLS. index.html gets Cache-Control: no-cache; content-hashed assets get long max-age.
User uploads PDF/DOCX/TXT
โ
โผ
fileParser.ts extracts plain text
(pdfjs-dist blob-URL worker for PDF; mammoth for DOCX; File.text() for TXT)
โ
โผ
Home.tsx assembles the request
- assemblePromptInstructions(enabledPrompts) โ system instruction block
- model IDs from selectedModels[]
โ
โผ
Promise.allSettled([analyzeWithModel(m1), analyzeWithModel(m2), ...])
โ
โโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ openrouter.ts โ per model: โ
โ - decryptKey(apiKeyEncrypted) โ
โ - AbortController (120s timeout) โ
โ - POST /v1/chat/completions โ
โ - response_format only for OpenAI models โ
โ - JSON.parse + schema validation โ
โ - ModelResult { status: "done" | "error" }โ
โโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
Results dashboard:
TrustScoreRing + Summary + Dimensions + Flagged Clauses
Per-model tabs ยท Export (PDF/MD) ยท Share URL
The decision to have zero server-side infrastructure was deliberate:
-
Privacy: The most common legitimate objection to using an AI contract analyzer is "I don't want my contract stored on someone's server." With Clippy, there is no server. The promise is verifiable โ the source code is public.
-
Cost: A backend adds infrastructure costs, maintenance burden, and operational complexity. For a free, open-source tool, those costs don't scale.
-
Trust: Every server is a potential attack surface. A contract often contains sensitive personal and business information. Removing the server removes the surface.
-
Speed: Direct browser-to-OpenRouter calls are faster than browser โ Clippy proxy โ OpenRouter.
The trade-off: users must provide their own API key. That's a friction point, but it's the honest trade.
User types key
โ
โผ (on "Lock")
encryptKey(plaintext)
โโโ crypto.subtle.generateKey(AES-GCM 256) โ sessionKey (in JS heap, never exported)
โโโ crypto.subtle.encrypt({ name: "AES-GCM", iv: random12bytes }, sessionKey, plaintext)
โโโ base64(iv + ciphertext + authTag) โ apiKeyEncrypted (stored in state)
Before each API call:
โ
โผ
decryptKey(apiKeyEncrypted)
โโโ parse iv + ciphertext from base64
โโโ crypto.subtle.decrypt({ name: "AES-GCM", iv }, sessionKey, ciphertext)
โโโ raw key string (used for this request only, then GC'd)
The AES CryptoKey lives only in the JavaScript heap. It is never exported, never serialized, and is wiped when the tab closes. The encrypted blob in state is useless without the session key. This means: the key is protected against casual inspection (DevTools copy(state), browser extensions logging React state), but not against a sophisticated in-process attack that can access the JS heap directly.
// Step 2: user has toggled objectives
const enabled = prompts.filter(p => p.enabled);
// In openrouter.ts:
function assemblePromptInstructions(prompts: AnalysisPrompt[]): string {
return enabled.map((p, i) =>
`## Objective ${i+1}: ${p.title}\n${p.prompt}`
).join("\n\n");
}
// Sent as part of the system prompt:
SYSTEM_PROMPT + "\n\n" + assembledInstructions + "\n\n## Contract Text\n" + contractTexttype AppStep = "setup" | "prompts" | "results";Three steps, one discriminant. When step === "results", the results array starts with all models in { status: "loading" } and updates live as each analyzeWithModel() Promise resolves or rejects. The Promise.allSettled() wrapper ensures one model's failure never blocks the others.
// Initial state when analysis starts
{ modelId, modelName, status: "loading" }
// On success
{ modelId, modelName, status: "done", trustScore, summary, flags, dimensions, durationMs }
// On error (does not block other models)
{ modelId, modelName, status: "error", error: "human-readable message" }This section is the legal background behind every analysis objective Clippy ships. Understanding the law helps you interpret the flags.
Most contracts you'll encounter in daily life are contracts of adhesion (contrats d'adhรฉsion) โ pre-drafted by the stronger party, presented on a take-it-or-leave-it basis, with no possibility of negotiation. Courts across the world have long struggled with how to protect consumers and weaker parties.
The core tension is between two principles:
- Freedom of contract (pacta sunt servanda) โ parties who freely agree to terms should be bound by them
- Substantive fairness โ courts should not enforce unconscionable or abusive contracts
The EU leans heavily toward consumer protection. The US leans toward freedom of contract with important carve-outs. Understanding which law applies to your contract is why Clippy's "Governing Law & Jurisdiction" prompt exists.
Council Directive 93/13/EEC of 5 April 1993 on unfair terms in consumer contracts is the foundational EU framework. It applies across all 27 member states and has been implemented into national law (France's Code de la consommation Art. L. 212-1, Germany's BGB ยงยง 307โ309, Spain's LGDCU).
Key provisions:
- Art. 3(1) โ An unfair term causes "a significant imbalance in the parties' rights and obligations, to the detriment of the consumer"
- Art. 5 โ Terms must be in plain, intelligible language. Ambiguous terms interpreted in the consumer's favour (contra proferentem)
- Art. 6(1) โ Unfair terms are not binding on the consumer; the rest of the contract survives
- Annex (Grey List) โ Terms that may be unfair include: unilateral modification rights; giving the seller exclusive right to interpret the contract; excluding liability for death or personal injury; auto-extending contracts with short opt-out windows; disproportionate compensation requirements
Key CJEU jurisprudence:
- Ocรฉano Grupo Editorial v. Murciano Quintero (C-240/98) โ Courts can examine unfairness of their own motion. Consumers don't have to raise it themselves.
- Aziz v. Caixa d'Estalvis (C-415/11) โ Clarified the "significant imbalance" test: would the consumer have agreed to this in individual negotiation? If not, it's unfair.
- RWE Vertrieb v. Verbraucherzentrale (C-92/11) โ Unilateral price variation clauses without transparent mechanism held unfair.
When Clippy fires a CRITICAL flag on a unilateral modification clause, it is flagging something EU law has repeatedly held unfair under 93/13/EEC.
The General Data Protection Regulation (25 May 2018) applies to any organisation processing personal data of EU residents. It has influenced CCPA (California), LGPD (Brazil), PDPB (India), and dozens of other national laws.
Key Articles for contract review:
| Article | Topic | What to check |
|---|---|---|
| Art. 5 | Principles | Data collected for specified, explicit purposes; not processed beyond those; stored no longer than necessary |
| Art. 6 | Lawful basis | Processing must have a lawful basis โ consent, contract performance, legitimate interest, legal obligation. Claiming "legitimate interest" for everything is a red flag |
| Art. 7 | Consent | Must be freely given, specific, informed, unambiguous. Pre-ticked boxes are not consent |
| Art. 13/14 | Transparency | What data, what purposes, what rights, what retention โ all must be disclosed at collection |
| Art. 17 | Right to erasure | The "right to be forgotten" โ data subjects can request deletion in defined circumstances |
| Art. 20 | Data portability | Right to receive personal data in machine-readable format and transfer it elsewhere |
| Art. 28 | Processor obligations | If you're contracting with a data processor, a Data Processing Agreement (DPA) is legally required |
| Art. 44โ49 | Transfers outside EU | Standard Contractual Clauses (SCCs) required for data transfers to non-adequate third countries |
US contract law has no single federal equivalent to EU Directive 93/13/EEC. Protection comes from:
1. The Unconscionability Doctrine (UCC ยง 2-302, Restatement Second ยง 208)
A clause may be void if it has both:
- Procedural unconscionability โ oppressive circumstances at formation (surprise, unequal bargaining power, no meaningful choice)
- Substantive unconscionability โ oppressively one-sided terms
Courts weigh these together. California courts are more willing to void terms; New York applies unconscionability sparingly.
2. Mandatory Arbitration and the FAA (9 U.S.C. ยง 1 et seq.)
The FAA strongly favours enforcement of arbitration agreements. Landmark SCOTUS decisions:
- AT&T Mobility v. Concepcion (2011) โ FAA preempts state laws invalidating class arbitration waivers. This effectively enabled companies to eliminate collective consumer redress via class-action waivers in arbitration clauses.
- American Express v. Italian Colors (2013) โ Even where individual arbitration costs exceed potential recovery, class arbitration waivers are enforceable.
- Viking River Cruises v. Moriana (2022) โ Limited PAGA (California's Private AG Act) in the arbitration context.
A mandatory arbitration clause with a class-action waiver in a US consumer contract can eliminate your practical ability to pursue small-value claims. Clippy flags these CRITICAL.
3. Non-Compete Enforceability by Jurisdiction
| Jurisdiction | Enforceability | Key Rule |
|---|---|---|
| California | Near-total ban | Cal. Bus. & Prof. Code ยง 16600 โ void as matter of public policy |
| Minnesota | Banned since 2023 | Minn. Stat. ยง 181.988 |
| North Dakota | Banned | ND Cent. Code ยง 9-08-06 |
| Florida | Strongly enforced | Presumption in favour of enforcement; courts may rewrite |
| New York | Moderate | "Reasonable" test โ time, geography, scope, legitimate interest |
| UK | Reasonable test | Must protect a legitimate interest; no wider than necessary |
| France | Must be compensated | Requires compensation during restriction period (Cass. Soc., 10 juillet 2002) |
Federal FTC rule (2024): The FTC issued a final rule seeking to ban most non-competes for workers โ facing immediate legal challenges; status should be verified against current law.
Consumer Rights Act 2015 (CRA):
- s.62 โ A term is unfair if it causes a significant imbalance in parties' rights and obligations, to the consumer's detriment, contrary to good faith
- s.64 โ Core terms exempt from fairness assessment if transparent and prominent
- s.65 โ Traders cannot exclude liability for death or personal injury caused by negligence. Period.
- s.67 โ Unfair term not binding; rest of contract survives
- Schedule 2 โ Grey list of potentially unfair terms
Unfair Contract Terms Act 1977 (UCTA) โ Still applies to B2B contracts. Section 11 sets a "reasonableness" test for exclusion clauses.
Key cases:
- OFT v. Abbey National [2009] UKSC 6 โ Bank charges were "core terms" and exempt from fairness test
- Director General v. First National Bank [2001] UKHL 52 โ "Significant imbalance" assessed over the overall contractual position
France has some of the world's most protective consumer contract law, built on the Code de la consommation:
Loi Chรขtel (2008):
- Art. L. 215-1 โ Suppliers with annual auto-renewal must notify consumers of their right to opt out 1โ3 months before the deadline. If not notified, consumer may terminate at any time.
Loi Hamon (2014):
- Extended cooling-off periods (14 days for most distance contracts)
- Strengthened prohibition on clauses abusives under Art. L. 212-1
Clauses abusives (abusive clauses):
- Art. L. 212-1 โ A clause creating a "significant imbalance" to the consumer's detriment is abusive
- Dรฉcret nยฐ 2009-302 โ Lists presumed abusive clauses (rebuttable) and a "black list" of clauses that are always abusive (e.g., excluding liability for bodily injury)
- Commission des clauses abusives (CCA) โ Issues influential sector-specific recommendations
What makes an arbitration clause problematic:
- Mandatory โ no choice but to arbitrate
- Class-action waiver โ cannot join others with the same claim
- Inconvenient seat โ arbitration far from the consumer's location
- Cost allocation โ consumer bears filing fees ($1,500+)
- Confidentiality โ results are secret, preventing public accountability
EU position: Mandatory pre-dispute arbitration in consumer contracts is generally unfair under 93/13/EEC โ it deprives consumers of court access guaranteed by Art. 47 of the EU Charter. CJEU repeatedly confirmed courts must examine arbitration clauses of their own motion.
US position: Post-Concepcion, mandatory arbitration + class waiver is generally enforceable. Notable exceptions: sexual harassment claims (Ending Forced Arbitration Act, 2022); some federal statutory claims.
UK position: Consumer Rights Act 2015 s.91 renders arbitration clauses unfair where the consumer must arbitrate claims below ยฃ5,000 without preserving the right to court.
UK: Under Patents Act 1977 (s.39) and CDPA 1988 (s.11), work created by an employee in the course of employment vests in the employer. "Course of employment" is narrowly construed โ personal time, personal equipment, unrelated topic is generally not employer property.
US: The "work made for hire" doctrine (17 U.S.C. ยง 101) is extremely broad for employees. But many contracts go further โ assigning all inventions, including personal ones. California Labor Code ยง 2870 provides a carve-out: employers cannot require assignment of inventions developed entirely on personal time, without employer equipment or trade secrets, unrelated to the employer's business. Several other states have similar provisions.
France: Copyright vests originally in the author (the employee) under the Code de la Propriรฉtรฉ Intellectuelle. Specific written assignments are required. Droit moral (moral rights) cannot be waived โ they are inalienable under French law.
What Clippy flags: Blanket "assign all inventions" clauses with no carve-outs for personal work; waivers of moral rights; post-employment invention clauses ("any invention created for 1 year after termination"); absence of specific compensation for IP assignment beyond base salary.
Typical limitation structure:
Total liability shall not exceed fees paid in the 12 months preceding the claim.
EXCEPT FOR: death/personal injury ยท fraud ยท wilful misconduct ยท IP infringement ยท
data breach ยท indemnification obligations
The carve-outs are where the exposure lives. A $1,000 cap with a carve-out for "any breach of IP warranties" is nearly illusory if you face third-party infringement claims.
Indemnification red flags:
- Broad triggers: "any claim arising from or relating to your use" = effectively unlimited
- Defense control: If the indemnifying party controls defense, they may settle on terms that bind you
- IP indemnification gaps: If a SaaS vendor's software infringes a patent and you get sued, are you covered?
- User content indemnification: Platforms requiring users to indemnify for any claims from user content
EU rules: Directive 93/13/EEC Annex items (a) and (b) prohibit excluding/limiting liability for death or personal injury โ these are in the "black list" and automatically unfair.
EU Rome I Regulation (593/2008): For consumer contracts, the chosen law cannot strip away protections from the mandatory rules of the consumer's country. An American company's ToS saying "governed by Delaware law" cannot deprive an EU consumer of rights under Directive 93/13/EEC and GDPR.
Brussels I Recast (1215/2012): For consumer contracts, the consumer may sue in courts of their own member state regardless of any exclusive jurisdiction clause.
US approach: Courts generally enforce forum selection clauses (The Bremen v. Zapata, 407 U.S. 1 (1972)), even for consumers. California has been reluctant to enforce outbound forum selection against California consumers.
"Mandatory arbitration in San Francisco" โ common in US tech ToS. For a user in France or Indonesia, pursuing a $200 claim means flying to California for arbitration under California law. In practice, this eliminates consumer recourse entirely.
What Clippy looks for: Jurisdiction clauses requiring dispute resolution in an inconvenient location; exclusive jurisdiction in the vendor's home courts; class-action waivers; choice-of-law clauses potentially stripping EU statutory protections.
Cognitive overload: The average Terms of Service document is 7,000โ10,000 words. Reading every ToS you encounter would require approximately 76 working days per year. No one reads them. This is rational, not lazy.
Information asymmetry: The drafting party employs lawyers who have optimised the contract over years of litigation. The signing party has no legal background and no time. This structural imbalance is why courts developed unconscionability, contra proferentem, and unfair terms legislation.
Contract length as strategy: Research suggests longer contracts increase the probability that problematic clauses go unnoticed. Placing a class-action waiver in paragraph 47 of a 50-page ToS is not accidental.
AI as a leveller: For under $0.15, you get the first-pass review a paralegal would charge $150 for โ instantaneous, parallel across 8 models, with specific clause quotation and severity tagging. That's the core proposition of Clippy.
What Clippy does well:
- Identifying structurally unusual or one-sided clauses
- Flagging clauses matching known patterns of abuse (mandatory arbitration, auto-renewal, unlimited indemnification)
- Summarising the overall risk profile of a contract
- Running the same analysis across multiple models for second and third opinions
What Clippy cannot do:
- Provide legal advice โ it is not a lawyer, and for significant contracts you should consult one
- Assess context โ a non-compete that's abusive for a junior employee may be reasonable for a C-suite executive
- Know the latest case law โ models have training cutoffs
- Analyse scanned PDFs โ text must be machine-readable
- Guarantee completeness โ AI can miss clauses; human review remains essential for high-stakes agreements
Threat model:
- Contract text is sent to OpenRouter and then to the AI provider. Treat this as "read by the provider's infrastructure." Do not analyze contracts containing national security secrets, privileged attorney-client communications, or highly sensitive third-party data.
- API key is encrypted in browser memory (AES-GCM). It is not stored to disk, never sent to Clippy infrastructure, sent to OpenRouter over TLS. This is defence-in-depth against casual inspection โ not against a capable browser-level adversary.
- Share URLs encode results in the URL fragment (base64). The analysis results, including flagged clause quotes, become public if you share the URL.
Five dimensions score each contract on a 0โ100 scale:
| Dimension | What it measures | Low score means |
|---|---|---|
| Transparency | Are terms in plain language? Are limitations clearly disclosed? | Dense legalese, buried definitions, obligations hidden in cross-references |
| Balance | Are rights and obligations roughly symmetrical? | One party has broad unilateral rights; the other has few or none |
| Legal Compliance | Does the contract conform to applicable law? | Terms violating GDPR, consumer protection law, or employment law |
| Financial Risk | Are financial obligations clear and proportionate? | Hidden fees, unlimited indemnification, disproportionate penalties |
| Exit Freedom | How easily can the weaker party exit? | Long notice periods, heavy termination fees, obligations surviving termination |
| Severity | Legal threshold | Example |
|---|---|---|
| CRITICAL | Clause likely unlawful, clearly abusive, or severely harmful | Mandatory arbitration + class waiver; GDPR Art. 6 lawful basis missing; auto-renewal with no cancellation right; unlimited liability exposure |
| SUSPECT | Unusual, one-sided, or potentially enforceable but outside market norms | 90-day notice period; non-compete with no compensation; arbitration in inconvenient city |
| MINOR | Worth noting but common and generally accepted | Standard limitation of liability; 30-day payment terms; governing law of vendor's home state |
OpenAI models receive response_format: { type: "json_object" }. Other models receive a prompt instruction. The fallback parser handles the common case where models wrap JSON in a Markdown code fence:
const cleaned = content
.replace(/^```json?\n?/, "") // strip opening code fence
.replace(/\n?```$/, "") // strip closing code fence
.trim();
parsed = JSON.parse(cleaned);Contract analysis benefits from very low temperature. temperature: 0.1 gives consistent, deterministic legal assessment while leaving enough flexibility for the model to adapt phrasing naturally. Higher temperatures introduce creative but inaccurate interpretations of legal language.
pdf.js v5 requires a Web Worker for PDF parsing. The worker file is .mjs โ a format that SiteGround's nginx serves as application/octet-stream by default (browsers refuse to load Workers with non-JS MIME types). The fix:
// Fetch the worker and re-wrap as a text/javascript Blob
const workerResponse = await fetch(workerUrl);
const workerText = await workerResponse.text();
const blob = new Blob([workerText], { type: "text/javascript" });
GlobalWorkerOptions.workerSrc = URL.createObjectURL(blob);.htaccess also has AddType text/javascript .mjs as a belt-and-suspenders fix for direct browser requests.
https://clippy.legal/#/share/BASE64_ENCODED_PAYLOAD
The payload is btoa(encodeURIComponent(JSON.stringify(SharePayload))). It contains: fileName, analyzedAt, prompt titles, and all ModelResults โ but never the API key. The SharePayload.version field is "2.0.0" โ this is the payload format version, intentionally frozen. Changing it breaks all existing share links.
A typical 2โ3 model analysis SharePayload is ~5โ15KB raw JSON. After encoding, ~7โ20KB. Modern browsers support URLs up to 2MB. Some email clients truncate at ~2000 chars โ prefer direct copy-paste for sharing.
| Score | Color | Label |
|---|---|---|
| 75โ100 | Green (#22c55e) |
Fair |
| 50โ74 | Yellow (#eab308) |
Caution |
| 30โ49 | Orange (#f97316) |
Risky |
| 0โ29 | Red (#ef4444) |
Abusive |
Calibrated against typical outputs from Claude and GPT-4.1 during development. Most real-world enterprise contracts land in the 55โ75 range. A "100" would be an unusually fair, plain-language, balanced contract.
These are the real problems encountered between v1.0.0 and v3.2.5. Each one taught something worth documenting.
v3.0.x โ v3.1.0 ยท PDF upload broken on production
PDF upload worked locally but threw a TypeError: Failed to construct 'Worker' on the live site. No error in the console on first load. Took a long time to diagnose because the local environment never triggered it.
Root cause: pdfjs-dist v5 switched its worker file from .js to .mjs (ES module). SiteGround's nginx serves .mjs as Content-Type: application/octet-stream. Browsers refuse to load a Worker from a non-JavaScript MIME type โ it's a security policy enforced at the platform level.
Additionally: the CDN worker URL used in v1 (cdnjs.cloudflare.com/...) was version 4.9.155, while the installed package was v5.6.205. The two were incompatible.
Fix: Fetch the .mjs worker file, re-wrap it as a text/javascript Blob, register the blob URL with GlobalWorkerOptions.workerSrc. The browser sees a correctly-typed Worker regardless of what the server says.
Lesson: When using a library that spawns a Worker, always verify the MIME type your server serves for that worker file. Nginx doesn't know about .mjs by default. Neither do many CDNs.
v3.1.x โ v3.2.0 ยท "No endpoints found" across 6 models
Multiple models returned "No endpoints found" immediately after v3.1.0 launched. Affected: Claude, GPT-4o, Gemini Pro 1.5, Mistral Large, Llama 3.1. Essentially all non-OpenAI models.
Root cause: OpenRouter renames model IDs as providers release new versions. The IDs hard-coded in v3.1.0 had all been superseded without announcement.
Fix: Audited every ID against the live OpenRouter models endpoint.
| Old (broken) | New (working) |
|---|---|
anthropic/claude-3.5-sonnet |
anthropic/claude-3.7-sonnet |
openai/gpt-4o |
openai/gpt-4.1 |
openai/gpt-4o-mini |
openai/gpt-4.1-mini |
google/gemini-pro-1.5 |
google/gemini-2.5-pro |
mistralai/mistral-large |
mistralai/mistral-large-2512 |
meta-llama/llama-3.1-70b-instruct |
meta-llama/llama-3.3-70b-instruct |
Lesson: Never hard-code OpenRouter model IDs and assume permanence. Review the live endpoint each major version bump.
v3.1.x โ v3.2.0 ยท All non-OpenAI models failing with API errors
After fixing model IDs, Claude, Gemini, Mistral, Llama, and DeepSeek still failed with cryptic errors. Different error messages per provider โ took a while to find the common cause.
Root cause: The code sent response_format: { type: "json_object" } to every model. This is an OpenAI extension. Anthropic, Google, Mistral, Meta, and DeepSeek don't support it โ and they return errors when they receive it.
Fix: Added a JSON_MODE_MODELS set of OpenAI model ID prefixes. The response_format field is only included when the model ID starts with openai/. All other models receive a prompt instruction to output JSON.
Lesson: The OpenAI Chat Completions API is a de facto standard, but its extensions are not universal. Always read each provider's specific docs before assuming OpenAI extensions work everywhere.
v3.1.x โ v3.2.1 ยท Cards stuck on "Reading..." forever
Some model cards would show "Readingโฆ" with no progress, no error, no feedback. Not all models โ just some, sometimes. Users reported it consistently.
Root cause: fetch() has no built-in timeout. If OpenRouter accepted the request but the upstream model was overloaded or slow, the response could take arbitrarily long with no signal to the client.
Fix: AbortController with a 120-second timeout on every model fetch.
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 120_000);
try {
const response = await fetch(url, { signal: controller.signal, ...options });
} finally {
clearTimeout(timeout);
}Lesson: Always set timeouts on fetch calls to external AI APIs. Models can queue indefinitely. A user staring at a spinner for 3 minutes with no feedback will just close the tab.
v3.1.x ยท Users stuck on old app version after deploy
After deploying a new version, users would still see the old app. Hard refresh fixed it consistently. Reproduced across browsers.
Root cause: Vite content-hashes JS and CSS bundles, so those cache correctly. But index.html has no hash โ it's always index.html. Without explicit Cache-Control headers on index.html, browsers cache it for hours, keeping users on stale JS bundles.
Fix: Cache-Control: no-cache, no-store, must-revalidate on index.html via both the HTML <meta> tag and .htaccess. JS/CSS assets get long max-age headers because their filenames change with content.
Lesson: Vite/React apps need two caching policies: no-cache for index.html, immutable (or long max-age) for content-hashed assets. Vercel and Netlify handle this automatically. FTP to shared hosting does not.
v3.1.x โ v3.2.1 ยท Locale links broken
Sharing https://clippy.legal?lang=fr would show the English UI. The parameter was completely ignored.
Root cause: detectLocale() in i18n.ts checked only localStorage and navigator.language. It never read the URL ?lang= parameter.
Fix: Updated detectLocale() to check URL params first (highest priority), then localStorage, then navigator.language, then "en" fallback. The chosen locale is persisted to localStorage so subsequent navigation keeps it.
Lesson: URL parameters must be the highest-priority locale signal. They're the only way to override locale programmatically โ for testing, share links, and regional marketing.
v3.0.x ยท Password error shown for every file parse failure
When a PDF upload failed โ for any reason โ the UI showed "Incorrect password." Users found this confusing because they hadn't entered a password.
Root cause: The catch block in the file parser always showed the same error message, regardless of what actually went wrong.
Fix: Changed the catch block to read err.message and route to the appropriate user-facing error string: password errors show the password message; scanned PDFs show a "text not extractable" message; file too large shows a size message; other errors show a generic parse failure.
Lesson: Never show a generic error in a catch block without reading the actual error. AI-adjacent apps have many failure modes โ users need accurate feedback to understand what went wrong and what to try next.
v3.2.1 โ v3.2.2 ยท Users reporting Claude "stuck"
Users reported Claude analysis appearing to hang even with the 120s timeout in place. Investigation showed the request was completing โ just taking 20โ35 seconds on typical contracts, compared to 3โ8 seconds for GPT-4.1 on the same input.
Root cause: Not a bug. claude-sonnet-4.6 was genuinely slow at the time of testing on the OpenRouter endpoint โ possibly due to demand or upstream queuing.
Fix: Swapped the default Anthropic model to claude-3.7-sonnet, which consistently returns in 3โ8 seconds on the same contracts. Timeout raised from 90s to 120s.
Lesson: Model speed is not guaranteed. Test with real contracts, not just short test paragraphs. Have a fallback. Always set a timeout.
v3.0.x ยท App crashed silently during analysis in non-English locales
Analysis appeared to work in English but would crash silently when triggered with the French locale active. No user-visible error โ the card would just never populate.
Root cause: const { t } = useI18n() โ the locale variable was not destructured from the hook, but it was referenced directly in the analysis call to pass the current language to the model. locale was undefined, which caused a silent failure in the API request assembly.
Fix: Added locale to the destructured variables from useI18n().
Lesson: Destructuring omissions in TypeScript are silent when the variable is used as a string interpolation argument rather than called as a function. Always destructure everything you reference, and lean on TypeScript's strict mode to catch missing variables early.
clippy/
โโโ client/
โ โโโ index.html # Vite entry point + no-cache meta + font preloads
โ โโโ public/
โ โ โโโ .htaccess # MIME types (.mjs โ text/js) + cache headers
โ โโโ src/
โ โโโ App.tsx # Router (/ and /share/:payload)
โ โโโ index.css # CSS variables, animations, global styles
โ โโโ components/
โ โ โโโ ClippyCharacter.tsx # Animated SVG paperclip mascot (3-layer depth effect)
โ โ โโโ TrustScoreRing.tsx # Animated SVG progress ring (stroke-dashoffset)
โ โ โโโ LanguageSwitcher.tsx # Flag grid dropdown for 20 locales
โ โโโ lib/
โ โ โโโ openrouter.ts # API client, model registry, system prompt, JSON mode gating
โ โ โโโ fileParser.ts # PDF (blob URL worker) + DOCX (mammoth) + TXT extraction
โ โ โโโ encryption.ts # AES-GCM 256-bit key encryption via Web Crypto API
โ โ โโโ prompts.ts # 10 curated analysis objectives, 5 categories
โ โ โโโ export.ts # PDF (jsPDF) + Markdown export
โ โ โโโ share.ts # Base64 URL encode/decode for SharePayload
โ โ โโโ i18n.ts # Custom i18n: 20 locales, t(), I18nProvider, detectLocale()
โ โโโ pages/
โ โโโ Home.tsx # 3-step wizard (~1500 lines): Setup โ Objectives โ Results
โ โโโ ShareView.tsx # Read-only shared result viewer
โโโ shared/
โ โโโ schema.ts # Shared TypeScript types (AppState, ModelResult, SharePayload)
โโโ server/ # Express dev scaffold (not used in production)
โโโ CHANGELOG.md
โโโ CONTRIBUTING.md
โโโ INSTALL.md
โโโ SECURITY.md
โโโ README.md
- Open
client/src/lib/openrouter.ts - Add to
AVAILABLE_MODELS:
{
id: "provider/model-name", // from openrouter.ai/models โ verify it's live
name: "Display Name", // shown in the model selection grid
provider: "Provider Name", // shown as label
description: "Short desc", // tooltip / card description
icon: "P", // single letter avatar fallback
}- If the model is OpenAI-compatible and supports
response_format, add its prefix toJSON_MODE_MODELSin the same file.
That's it. The model appears in the grid automatically. Find current IDs at openrouter.ai/models.
client/src/lib/prompts.ts, in DEFAULT_PROMPTS:
{
id: "unique-id",
title: "Short display title",
description: "One-line description shown to users",
prompt: `The actual instruction text sent to the model.
Cite specific laws, articles, and case law for best results.`,
category: "general" | "privacy" | "financial" | "employment" | "ip" | "custom",
enabled: boolean, // true = on by default
isDefault: boolean, // true = part of the shipped library
isCustom: boolean, // true = user-created, can be deleted
}SYSTEM_PROMPT in client/src/lib/openrouter.ts. If you add new JSON output fields, update the relevant types in shared/schema.ts and the export formatters in client/src/lib/export.ts.
Approximate cost per model for a 5,000-word contract (3 default prompts, ~8k tokens in, ~1.5k out):
| Model | Estimated cost |
|---|---|
claude-3.5-haiku |
~$0.002 |
gpt-4.1-mini |
~$0.002 |
llama-3.3-70b-instruct |
~$0.003 |
mistral-large-2512 |
~$0.008 |
gemini-2.5-pro |
~$0.012 |
gpt-4.1 |
~$0.030 |
claude-3.7-sonnet |
~$0.035 |
Running all 8 models: roughly $0.10โ0.15 total. The two cheapest models (Haiku + GPT-4.1 Mini) together cost under $0.01. Enabling additional objectives (Step 2) increases input tokens proportionally.
Prices change. Check openrouter.ai/models for current pricing.
- v1.0.0 โ Multi-model analysis, trust score, 5 dimensions, severity flags
- v2.0.0 โ 3-step wizard, modular prompt library, AES-GCM encryption, PDF/MD export, share URLs
- v3.0.0 โ Full i18n (17 languages), RTL support, locale-aware AI output
- v3.1.0 โ See Demo modal, version badge, README illustration
- v3.2.0 โ PDF upload fix (blob URL worker), updated model IDs, password manager suppression
- v3.2.1 โ
?lang=URL param, accurate file error messages,response_formatfix, 90s timeout - v3.2.2 โ claude-3.7-sonnet default, 120s timeout, no-cache
index.html - v3.2.3 โ Full code audit, improved JSDoc, comprehensive README rewrite
- v3.2.4 โ 20 languages (added Turkish ๐น๐ท, Swedish ๐ธ๐ช, Indonesian ๐ฎ๐ฉ), README mega-rewrite with screenshots
- v3.2.5 โ Clippy shows "analyzing" message while working, switches to results message when done
- Side-by-side diff view between model results
- Clause-by-clause highlighting (map flagged clauses back to source text)
- Template prompt sets (SaaS ToS, employment, real estate, NDA)
- Optional self-hosted backend with persistent analysis history
After npm run build, dist/public/ is a fully self-contained static site:
- Cloudflare Pages โ Free, global CDN, automatic deploys from GitHub
- Vercel / Netlify โ One-click deploy, free tier, handles cache headers automatically
- GitHub Pages โ Push
dist/public/contents to agh-pagesbranch - Any FTP server โ Upload
dist/public/contents to your webroot - S3 / R2 bucket โ Enable static website hosting, upload contents
- Docker + nginx โ See
INSTALL.mdfor a complete Dockerfile
See INSTALL.md for step-by-step guides.
See CONTRIBUTING.md. Areas where help is especially useful:
- More OpenRouter models โ 5 minutes, immediate value for all users
- Prompt improvements โ better flagging accuracy, more jurisdictions, contract-type specializations
- Additional translations โ UI ships in 20 languages; more are welcome
- Accessibility โ keyboard navigation, screen reader support, ARIA labels
- Testing with edge-case contracts โ non-English contracts, unusual formats, PDFs from different generators
Found a security issue? See SECURITY.md.
Since Clippy runs entirely client-side and stores no data, the attack surface is minimal. The main consideration is ensuring the OpenRouter API key is never persisted in ways readable by other scripts. In v2+, the key is AES-GCM encrypted in browser memory โ see client/src/lib/encryption.ts for the full technical details and honest caveats.
MIT โ see LICENSE.
Fork it. Hack it. Make it better. That's the point.
- AI routing โ OpenRouter
- PDF parsing โ pdf.js by Mozilla
- DOCX parsing โ mammoth.js by Mike Williamson
- PDF export โ jsPDF
- UI framework โ React + Vite + Tailwind CSS + shadcn/ui
- Namesake โ Clippy the Office Assistant, Microsoft Office 97โ2003
- EU Directive 93/13/EEC โ EUR-Lex
- GDPR 2016/679 โ EUR-Lex
- California CCPA โ California AG
- UK Consumer Rights Act 2015 โ legislation.gov.uk
- Federal Arbitration Act (9 U.S.C.) โ Cornell LII
- AT&T Mobility v. Concepcion, 563 U.S. 333 (2011) โ Supreme Court
- Rome I Regulation (EC 593/2008) โ EUR-Lex
- California Labor Code ยง 2870 โ California Legislature
- French Code de la consommation โ Lรฉgifrance
This project is 100% vibe coding.
I am not a software engineer. I have no CS degree, no MSc, would not pass a LeetCode interview, and I'm not pretending otherwise. I'm a French entrepreneur โ a former hacker turned product person โ who has always had a healthy obsession with technology and a very good working relationship with AI tools.
Every line of TypeScript in this repository was written by an AI. Every architecture decision was a conversation. Every bug fix was describing what was broken and letting the model figure out why. The AES-GCM encryption model, the prompt library, the share URL design, the jsPDF export pipeline, the blob URL worker strategy, the
response_formatgating โ all of it emerged from iteration, not from a textbook.
The AI wrote the code. But code is not product. Here's what wasn't AI:
Domain instinct. I knew why this product needed to exist before I knew how to build it. Contracts are the most impactful documents most people ever sign, and almost nobody reads them carefully. That conviction didn't come from a language model โ it came from years of signing contracts, negotiating, watching people get caught out by clauses they never noticed. The legal depth in this README isn't copied from a textbook โ it reflects accumulated domain understanding.
Taste and craft. When the first version of the UI looked like every other React app โ cold greys, standard card layout, forgettable โ I knew it needed to be different. The warm cream palette came from thinking about what reading a contract feels like. The Clippy mascot came from understanding that the product's personality was as important as its function. The speech bubbles, the blink animation, the "vibe" of the thing โ these are product decisions, not engineering ones.
Debugging patience. Most of the war stories in this README involved hours of iteration. The MIME type bug took a long time to diagnose because the local environment never triggered it โ only production did. Each of those sessions required understanding what was wrong before telling the AI what to fix. The AI wrote the fix. The human read the error, formed the hypothesis, and directed the search.
Editing and curation. Every AI output needs a human to decide what to keep, what to discard, what to push back on. The prompt library was written and rewritten multiple times because early versions were too generic. The severity calibration was tuned against real contracts, not just theoretical ones. The war stories section exists because I believed the bugs were worth documenting, not just fixed and forgotten.
The product roadmap. The decision to add 17 then 20 languages wasn't an AI suggestion โ it was a product decision about who Clippy should serve. The decision to make the analysis objectives modular (not hardcoded) came from watching people want to ask questions that weren't in the default set. The share URL feature came from observing that people reviewing contracts often want a second opinion from a colleague. None of these were in any original spec.
Working with AI coding tools at this level is a specific skill. It's not "just describe what you want." It requires:
- Context management โ knowing what the AI knows, what it has forgotten, and what it needs to be reminded of
- Hypothesis formation โ when something breaks, forming a plausible root cause hypothesis before asking for a fix
- Scope control โ AI tools have a tendency to over-engineer or refactor things that didn't need changing; knowing when to say "just fix the specific thing, don't touch the rest"
- Quality judgment โ distinguishing between code that is correct and code that is correct and maintainable
- Persistence โ the first solution is rarely the right one; good results come from multiple iterations
Clippy is a case study in what's possible when domain expertise, product instinct, and AI tooling are combined in a deliberate way. The AI was an extraordinarily capable pair programmer who never got tired, never got annoyed, and could context-switch from TypeScript to legal French to nginx MIME types without complaint. But it needed a human to set the direction, evaluate the output, and care about whether the end result was actually good.
The barrier between "I have an idea" and "the thing exists" has collapsed. Clippy is proof of that. A project that would have required a team of three engineers and a lawyer to produce is now possible for a single person with domain knowledge and taste.
But this doesn't mean engineering depth is irrelevant. It means it's redistributed. The AI provides the implementation depth. The human provides the product depth. That's a different pairing than the traditional "PM writes the spec, engineer writes the code" โ it's closer to a single person who can hold both simultaneously, mediated by a tool that makes the technical side tractable.
Vibe coding is not a shortcut. It's a different kind of work โ one that requires deep engagement with the output, constant iteration, and the willingness to be wrong many times before being right. What it removes is the prerequisite knowledge barrier. You don't need to know AES-GCM internals before you can build an app that uses AES-GCM encryption. You need to know why you need encryption, understand the security model well enough to verify the implementation, and have the patience to iterate until it's correct.
That's a trade worth celebrating โ and worth being honest about.
Built by paulfxyz ยท MIT License ยท clippy.legal
"It looks like you're signing a contract. Would you like help checking for nasty clauses?"
