[KongchangAI]
· 4 min read· 2,403 words

Local AI Tool Moonlight: Using Open-Source Models to Review Non-Compete Clauses So Your Side Hustle Doesn't Get You in Trouble

Local AI Tool Moonlight: Using Open-Source Models to Review Non-Compete Clauses So Your Side Hustle Doesn't Get You in Trouble

Open-source tool Moonlight uses a small local model to review employment contracts, extracting quotes without making judgments so you can see non-compete terms before starting a side hustle.

Moonlight is an open-source local AI tool built by developer Arpan Patra to help people review non-compete restrictions, IP ownership, and disclosure obligations in employment contracts before starting a side hustle. It runs entirely locally (Node.js + Express + Ollama + gemma3:4b) and uses an airgap script to intercept socket, TLS, and DNS calls; in testing, all 7 outbound connections pointed to the local loopback address. Architecturally, retrieval relies on keyword scoring, and the model only handles verbatim extraction—every quote must pass a substring check against the original text, or it's marked as "located, not read." An evaluation of 14 labeled clauses showed gemma3:4b had only 50% classification accuracy but 100% extraction accuracy, prompting the author to remove model judgment and switch to deterministic rule-based flags. The smaller gemma3:1b deleted "without the Company's prior written consent" in the very first test clause and was blocked by verbatim verification. The tool explicitly does not provide legal judgments.

Local AI Tool Moonlight: Using Open-Source Models to Review Non-Compete Clauses So Your Side Hustle Doesn't Get You in Trouble

When many developers want to start a side hustle, the first hurdle isn't writing code—it's digging out that long-forgotten employment contract. Clauses about non-compete restrictions, intellectual property ownership, and disclosure obligations may directly determine whether you can legally sell what you create. Developer Arpan Patra built the open-source tool Moonlight for this: a fully local AI assistant that scans contracts, extracts relevant clauses, and highlights noteworthy original wording.

Taking a Step Back from "Already Started a Side Hustle"

According to the Hacktoberfest challenge records on the DEV community, among nearly a hundred entries this week, tools for ticket classification, invoice extraction, and Etsy copy generation emerged in droves—but the vast majority assume you've already started your side hustle. Moonlight goes the opposite direction, targeting the step before launch: what does your employment agreement actually allow you to do?

This positioning has a real basis. Most people with full-time jobs only skim their contracts when signing and never open them again. Yet the IP clause in a contract may state: all works conceived, created, or developed by the employee during employment—whether or not during working hours, whether or not using company resources, and whether or not related to the company's current business—are exclusively owned by the company. Once such wording is overlooked, ownership of side-hustle output becomes a hidden risk.

Moonlight does not make legal judgments. It does only three things: find potentially relevant clauses, quote the original wording, and flag questions worth asking. This boundary is deliberately designed and directly shaped the subsequent technical decisions.

Local-First: Contracts Shouldn't Leave Your Computer

Moonlight's most core design choice is running entirely locally. No hosted demo, no cloud API. The reason is straightforward: employment contracts contain salary, employer information, notice periods, and specific obligations—once uploaded to a third-party service, this constitutes a privacy risk.

On the tech stack, the project uses Node.js, Express, and Ollama, running the gemma3:4b model, with only one application dependency. Starting it takes just three commands: pull the model, start the Ollama service, and npm install && npm start.

To verify that "local" isn't just a slogan, the author wrote src/airgap.js, which intercepts socket.connect, tls.connect, and dns.lookup calls before the app loads, then runs a full review to detect any connections to non-loopback addresses. Measured result: all 7 outbound connections pointed to 127.0.0.1:11434, and DNS queries were 0.

The Moonlight interface reviewing contract clause 9.2

The screenshot shows Moonlight reading clause 9.2 of a contract: beneath the original sentence hang four markers, corresponding to "beyond working hours," "may cover unrelated work," "uses personal devices," and "transfer of ownership," each traceable to the specific wording that triggered it.

Ollama is an open-source local large-model runtime that bundles model weights, an inference engine, and a simple HTTP API, letting developers run open-weight models like Llama, Gemma, and Mistral on consumer hardware with a single command. Moonlight relies on Ollama's /api/generate endpoint to send requests to gemma3:4b, which by default listens on 127.0.0.1:11434.

The emphasis on this point is because whether "local" holds depends on where inference requests are actually sent. Ollama by default binds only to the loopback address and won't forward prompts to the cloud; but some client libraries fall back to public endpoints when configuration is missing. Moonlight's airgap script verifies exactly this layer: it intercepts Node's socket, TLS, and DNS calls, confirming that all connections land on 127.0.0.1:11434, not some remote inference service. In other words, Ollama provides the local inference capability, and airgap provides a verifiable constraint on that capability—both are indispensable.

Minimizing the Model's Work

Moonlight's architecture can be summed up in one sentence: retrieval relies on rules, the model only handles extraction, and every excerpt must be verified against the original text. The model never sees the full contract from start to finish, nor does it decide which clauses matter.

Keyword Retrieval Filters Out Most Noise First

The contract is first split by clause number and scored using seven categories of keywords: outside work, IP assignment, exclusivity, non-compete, non-solicitation, confidentiality obligations, and disclosure obligations. A clause must contain an anchor phrase to become a candidate; otherwise generic wording like "shall not" or "in writing" would match a large amount of irrelevant content. In the example, a 28-clause contract was reduced to 12 candidates.

Anchor phrase here refers to characteristic wording that almost inevitably appears in a given type of clause—for example, "hereby assigns" in IP assignment clauses, "shall not engage" in non-competes, and "shall disclose" in disclosure obligations. Its role is not to understand semantics but to exclude obviously irrelevant clauses at extremely low cost.

The difference from generic keyword matching lies in its precision orientation: ordinary keywords match on a single word, while anchor phrases require an entire phrase to appear, thereby filtering out expressions like "shall not" and "in writing" that are high-frequency but non-distinguishing in legal texts. The cost is recall: clauses rewritten with unconventional wording may be missed for lacking standard anchors. Moonlight openly states this trade-off in the interface (not finding something doesn't mean it doesn't exist), letting users know where the boundaries of retrieval results lie. For a small local model, this pre-filtering also has an engineering dimension: the number of clauses entering the model's context drops from 28 to 12, directly reducing the probability of a small model "stopping halfway through output" on long texts.

Excerpts Must Pass Original-Text Verification

The model has only one task: copy key sentences from the clause. The system then checks whether the returned quote actually appears in the original text—normalizing whitespace and doing a string comparison. If the quote is not in the original, that reading is invalidated, but the clause itself is still displayed and marked as "located, not read."

This verification targets the most dangerous error in legal texts: a plausible-sounding paraphrase that may quietly change the meaning of a clause, while the reader may not notice that a key qualifier has disappeared.

Verbatim verification is a strict variant of the "attribution" approach in retrieval-augmented generation. The common practice is to let the model include citations and then have a program check whether the citations actually exist; Moonlight pushes this step to the extreme—the model's only permitted output is to copy sentences verbatim from the clause, and any string not in the original is treated as a failed reading.

The specific comparison method is: normalize whitespace (collapse consecutive spaces and newlines into a single space) for both the model's returned quote and the contract's original text, then perform substring matching. It does not handle synonym substitution, word-order adjustment, or rewrites beyond punctuation differences, so once the model "polishes" a sentence it gets caught. This design targets the most dangerous failure mode in legal texts: a smoothly reading paraphrase may quietly drop qualifiers like "without the Company's prior written consent," turning "may apply for permission" into "absolutely prohibited." The cost of verification is recall—even if the model read correctly and just added one extra character, it is judged as not read; but for this type of tool, better not to display than to display a sentence with altered meaning.

One Evaluation Led the Author to Remove the Model's Judgment

The initial version of Moonlight had gemma3:4b classify each clause into three categories: whether it applies outside work, answering yes, unclear, or no. All 8 tested clauses returned unclear. After rewriting the prompt and adding two examples, the same model returned yes for all of the same 8 clauses.

The model didn't change, the contract didn't change—only the wording changed. This output isn't a judgment; it's more like a coin landing in the direction the prompt tilted. Rather than continuing to tune prompts by intuition, the author built a small evaluation set: 14 labeled clauses, 6 that genuinely extend beyond work, 5 that don't, and 3 that are inherently ambiguous.

gemma3:4b's results pointed in two opposite directions:

  • Classification accuracy 50%: 13 of 14 clauses were judged yes, and even a vacation-leave clause might be flagged as affecting side-hustle income.
  • Original-text extraction accuracy 100%: all 14 quotes matched verbatim, with no paraphrase or fabrication.

So the classification feature was removed and the extraction feature retained. In its place, src/flags.js uses pattern matching to identify specific attributes and cite the triggering phrase: requiring written consent rather than outright prohibition, remaining effective after employment ends, extending beyond working hours, potentially covering unrelated work, transfer of ownership, etc. Flags can still be wrong, but the errors are visible—users can inspect the specific phrase and judge for themselves.

The evaluation also exposed another notable detail: in this small sample, no error of "judging a binding clause as harmless" occurred (yes→no was 0). For this type of tool, the cost of missing a restrictive clause may be greater than making the user take an extra look.

Small-sample evaluation sets are the engineering method most worth learning from here. The author did not repeatedly tune prompts by intuition but manually labeled 14 clauses (6 that genuinely extend beyond work, 5 that don't, and 3 that are inherently ambiguous), turning "is this model reliable?" into a repeatably measurable number. Although the sample is small, it was enough to expose directional problems: 50% classification accuracy and 13 of 14 judged yes showed that the model's output hardly varied with clause content and was more like being carried along by the overall tendency of the prompt.

The evaluation set also served as a regression test. After switching models (from 4b to 1b), modifying prompts, or adjusting flags rules, running the same set of clauses revealed whether behavior had degraded. It also provided the basis for the decision to "remove the classification feature"—not because classification occasionally erred, but because it failed simultaneously in two opposite directions (also judging the vacation-leave clause as yes), indicating that this capability simply doesn't hold at the 4b scale. Publishing the evaluation results and failure samples in the article lets readers judge the scope of applicability of the conclusions, rather than accepting only an assertion of "good results."

Small Models Exposed a More Dangerous Failure

Because the model runs locally, switching models is just changing an environment variable. gemma3:1b is 815MB, compared with 3.3GB for gemma3:4b, and is 15% faster, but its performance on the same evaluation set was: 43% classification accuracy and 0% original-text extraction accuracy.

The failures fall into two types. On long clauses, the model output one key and stopped—requested five keys, returned only {"governs_side_income":"yes"}, ending after 11 tokens with done_reason: stop. Since there was no quote to verify, Moonlight marked it as "located, not read," displayed the original text, and did not pretend the reading succeeded.

The failure on short clauses was more insidious. The model gave a plausible-sounding quote: "The employee shall not engage in any other employment, consulting, trade, or business, with or without compensation." But the contract's original text was: "The employee shall not, without the Company's prior written consent, engage in any other employment, consulting, trade, or business." The model deleted the seven words "without the Company's prior written consent."

This deletion turned "may apply for permission" into "absolutely prohibited." Someone reading the wrong version might give up on starting a business entirely. Verbatim verification intercepted this output because the generated sentence simply does not appear in the document. The author specifically notes that this is not a fabricated example to support the rules—this error appeared in the very first clause tested with the small model.

Open weights make this behavior checkable: switch models, run the same test set, and observe failure modes, rather than relying only on model cards or public benchmarks.

Code Review Also Found Problems More Serious Than Tokenization

When an agent was asked to scan the codebase for bugs, four rounds of checks found no memory leaks but exposed three more serious problems.

First, the clause-number regex made punctuation optional, causing any newline starting with a digit to be treated as a new clause. In the example, a clause about working hours was split into three segments, one of which was a 71-character fragment below the 40-character minimum and was discarded entirely. For a tool that promises "I found the clause," displaying a clause missing its final obligation is the worst kind of failure.

Second, the "no clause of a certain type found" message was computed after applying the candidate cap. With a cap of 4, it reported three types missing; with a cap of 100, it reported only one type missing—the interface would say "cannot be found anywhere in this document" about clauses that actually exist.

Third, when the model returned a bare null instead of an object, a TypeError was thrown, escaping from readOne and unwinding the entire review(), losing the results of 12 clauses due to a single formatting error.

After the three problems and four others were fixed, all 56 tests passed, and the airgap check still confirmed zero external network access. The author also changed one detail: the displayed quote now uses the document's own characters directly rather than the string returned by the model—because verification normalizes case, and when the model answers in all caps, displaying it would look like a verbatim quote from the original.

It's Not a Lawyer, and Doesn't Intend to Be One

Moonlight explicitly refuses to give legal judgments. If the model generates conclusions on its own, stripVerdicts() removes them. The "0 verdicts given" in the interface is a count of application behavior, not a marketing slogan.

Retrieval is keyword-based, and clauses written with unconventional wording may be missed. Not finding something doesn't mean it doesn't exist—this is clearly stated in the interface. The correct usage is: treat it as a first-pass reading aid, verify quotes, and consult a professional when legal advice is needed.

The author tested it with a real contract and found three clauses previously skipped, among which the "requires permission rather than outright prohibition" flag most changed his understanding of the wording. The sample contract in the repository is fictitious and only used to run the code.

The project is open-sourced under the MIT license at GitHub's imarpanpatra/moonlight. It can run gemma3:4b on an RTX 3050 with 4GB of VRAM. The entire project revolves around a simple idea: when documents are sensitive, the model should run on the machine that holds the documents.

#Moonlight#local AI tool#employment contract review#non-compete clauses#open-source AI#Ollama#gemma3#side hustle legal risk
Share:

Related articles