Cookbook
August 5, 2026 · View on GitHub
Practical recipes for plugging patina into existing writing and CI workflows. Each recipe is self-contained — copy, adapt, run.
For the full flag list see patina --help and CLI.md. For Document Type, Persona, and Register see README.md.
1. Batch-score a Hugo content folder
You have a Hugo site with many drafts under content/posts/. You want a quick AI-likeness scan over the whole folder before publishing.
# from your Hugo project root
patina --lang en --score --offline --batch content/posts/*.md
--batch treats every positional arg as an input file, so any glob your shell expands works. --offline makes this a reproducible local check with no backend. Omit it when you want the LLM-judged categories reconciled with the same deterministic signals.
For a stricter sweep that flags anything above 30/100, fail the run instead of just printing:
patina --lang en --score --offline --exit-on 30 --batch content/posts/*.md
When any file's overall exceeds the gate, patina exits with code 3 (CLI.md §Exit codes), which is perfect for a pre-publish check.
2. GitHub Actions integration (minimal workflow YAML)
Run patina as a non-blocking quality check on every PR that touches markdown.
# .github/workflows/patina.yml
name: Patina score
on:
pull_request:
paths: ["**/*.md"]
jobs:
score:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: |
git clone --depth 1 https://github.com/devswha/patina.git /tmp/patina
cd /tmp/patina && npm install --omit=dev && npm link
- name: Score changed markdown
run: |
changed=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- '*.md')
[ -z "$changed" ] && echo "no markdown changes" && exit 0
patina --lang en --score --offline --exit-on 30 --batch $changed
Drop --exit-on while you calibrate the threshold for your project. Remove --offline and configure a backend only if the workflow needs LLM-judged categories; see AUTHENTICATION.md.
3. Compare Claude vs Gemini output manually
When you want to compare how two backends rewrite the same paragraph, run them side by side and diff the outputs directly:
patina --lang en --backend claude-cli draft.md > /tmp/claude.txt
patina --lang en --backend gemini-cli draft.md > /tmp/gemini.txt
diff /tmp/claude.txt /tmp/gemini.txt
This keeps the comparison explicit: you can read both rewrites, inspect which one preserves your meaning better, and keep whichever voice you prefer.
4. Investigate a false positive with --diff --audit
A sentence got rewritten when you wanted it preserved. To see exactly which pattern fired, run audit and diff against the same input:
patina --lang en --audit draft.md # which patterns the scanner thinks fired
patina --lang en --diff draft.md # pattern-by-pattern before/after
Cross-reference the firing pattern IDs against PATTERNS.md. If a pattern is consistently mis-firing on your domain (e.g. legal boilerplate that legitimately uses "fundamentally"), add it to skip-patterns in your config:
# .patina.yaml
skip-patterns:
- en:7 # AI vocabulary words — too aggressive for legal prose
skip-patterns is a list key that merges additively across default / global / project configs, so the project-level skip doesn't lose the defaults.
5. Create a custom Document Type policy
Document Type owns genre, purpose, structural conventions, and pattern policy. Persona and Register remain independent. In a source checkout or managed patina installation, start from the nearest built-in policy:
cp document-types/blog.md document-types/my-newsletter.md
Change the frontmatter id so it matches the filename, define the document
conventions, then tune pattern-overrides:
---
document-type: my-newsletter
name: Internal newsletter
version: 1.0.0
scope: weekly engineering newsletter
purpose: summarize shipped engineering work and the next actions
audience:
- engineers and internal stakeholders
structure:
- lead with shipped changes, then impact, risks, and explicit next actions
style:
- use concrete component names and evidence
avoid:
- inventing delivery dates, owners, metrics, or commitments
pattern-overrides:
en:
14: suppress
7: amplify
---
Then opt in per run:
patina --lang en --document-type my-newsletter post.md
suppress is enforced deterministically by removing that pattern from the
prompt. reduce and amplify currently document policy intent but do not change
a runtime weight. For a project-local exclusion that does not require a custom
Document Type file, use skip-patterns in .patina.yaml.
6. Pre-commit hook wrapper (optional)
Block commits that introduce too-AI-sounding markdown. Drop this into .git/hooks/pre-commit (or wire it up through pre-commit/husky if you already use them):
#!/usr/bin/env bash
set -euo pipefail
changed=$(git diff --cached --name-only --diff-filter=ACM -- '*.md')
[ -z "$changed" ] && exit 0
patina --lang en --score --offline --exit-on 30 --batch $changed
--exit-on returns exit code 3 when any file's overall exceeds the threshold, which the shell treats as failure and aborts the commit. To bypass once (e.g. you intentionally want hype copy), commit with --no-verify.
7. Run patina against a local model (Ollama)
patina's openai-http backend works with any OpenAI-compatible server, so a local
Ollama instance plugs in without code changes:
PATINA_API_KEY=ollama patina --lang ko \
--backend openai-http --base-url http://localhost:11434/v1 \
--model gemma3:12b-it-qat --verify --timeout-ms 900000 draft.md
PATINA_API_KEY can be any non-empty string — Ollama ignores it, but the HTTP
backend requires one.
Three pitfalls, all observed in practice:
- Context size. patina's rewrite prompt (pattern digests + Document Type +
voice guidance) can exceed a small local model's default context. Newer
Ollama versions fail loudly (
exceed_context_size_error); older versions may silently truncate the prompt, which degrades rewrite quality. Start the server withOLLAMA_CONTEXT_LENGTH=24576(or higher) before testing. --verifyis not optional for small local models. A 12B model will happily round38%to "nearly 40%" and drop the survey year while producing an otherwise fluent rewrite.--verifyruns the MPS/fidelity floors plus the deterministic numbers-preserved guard and fails closed to the original when the rewrite mangles facts. Cloud-scale models rarely trip these floors; local 12B-class models do.- Broken GGUFs exist. If the output is a stream of
<unused12><unused7>…tokens, the model artifact itself is corrupt (bad quant/merge or tokenizer mismatch) — no patina flag will fix it. Verify with a bare one-line prompt against the server before blaming the pipeline.
Set --timeout-ms generously: a 12B model on an 8 GB GPU takes minutes per rewrite
at full prompt length, not seconds. When the per-attempt budget exceeds 300s, the
HTTP backend automatically switches to SSE streaming so Node's undici
headersTimeout cannot kill a slow local generation mid-flight (#576). Judge
candidates with the deterministic score (patina --score --offline on the
rewrite output), not vibes — and compare against a cloud backend baseline on the same input.
Where to go next
- Axis reference:
README.md - Backend setup:
AUTHENTICATION.md - MPS and other terms:
GLOSSARY.md - Adding patterns or false-positive triage:
CONTRIBUTING.md