Research CLI · ocl

Check citations and keep evidence from the command line

ocl is a small, dependency-free client for the OpenCaseLaw research API. It serves a lawyer at a terminal and an AI agent in a script alike: readable text on screen, JSON when piped. Everything it shows comes from the service unchanged: identifiers, citation strings, passage text.

Install

Python 3.10 or newer. Install it from PyPI as a tool with pipx or uv; both create the isolated environment that current Python installations require. ocl doctor checks the connection, the server and the client.

pipx install opencaselaw-cli        # or: uv tool install opencaselaw-cli
ocl --help

Without pipx or uv, use a virtual environment: python3 -m venv .venv && .venv/bin/pip install opencaselaw-cli, then run .venv/bin/ocl. Upgrade with pipx upgrade opencaselaw-cli (or uv tool upgrade opencaselaw-cli).

1. Check the citations in a draft

Put the references from your draft in a file, one per line, or as JSON lines when a reference carries a pinpoint. Each comes back as resolved, missing or ambiguous with the decision it found; a pinpoint counts as resolved only if that Erwägung exists in the index.

cat > references.jsonl <<'JSONL'
{"reference":"BGE 136 III 513","pinpoint":"2.3"}
{"reference":"4A_747/2012"}
{"reference":"BGE 999 III 1"}
{"reference":"BGE 140 III 86","pinpoint":"2.3"}
JSONL
ocl citations resolve --input references.jsonl

At a terminal this prints:

resolved              BGE 136 III 513  bge_BGE_136_III_513  E. 2.3 retrieved
resolved              4A_747/2012      bger_4A_747_2012     BGer 4A_747/2012 vom 5. April 2013
missing               BGE 999 III 1                         not in the corpus
pinpoint_unavailable  BGE 140 III 86   bge_BGE_140_III_86   E. 2.3 not in the index

partial: 2 resolved, 1 missing, 1 pinpoint_unavailable.
Existence and pinpoints only; no assessment of legal support.

The exit code is 4 because not everything resolved. What the check does not do: it never says that a decision supports your proposition, is still good law, or fits your facts. That reading is yours.

2. Keep the evidence behind a memo

One command runs the search and saves the decisions it selects, the Erwägungen you name and the statute articles you name into a folder: full texts as served, a plain-language INDEX.md, and a manifest.json with every request, timestamp, source link and file hash. Saved files are never overwritten; --resume finishes an interrupted run.

ocl bundle create 'Rachekündigung Art. 336 OR' --max-results 10 --passage 2 \
  --law OR:336 --out rachekuendigung-2026-09

The folder is readable without any tool:

rachekuendigung-2026-09/
  INDEX.md                                    what was saved, in plain language
  manifest.json                               every request, timestamp, hash, source link
  decisions/bge_BGE_136_III_513-5b3e22ef.txt  full text as served (+ .json metadata)
  passages/bge_BGE_136_III_513_2-ff487655.txt E. 2, verbatim
  laws/OR_336-812a5382.json                   Art. 336 OR, consolidation date, Fedlex link
  search/page-0000-1850ce82.json              the search page the selection came from

A bundle is complete when every requested item was saved, otherwise partial with each failed item listed, for example a cantonal decision without numbered Erwägungen. It preserves what the service returned on that day; the corpus is rebuilt nightly, so the same query next month can select other decisions.

What the results mean

  • resolved: the decision exists in the corpus. With a pinpoint, the named Erwägung exists and its text is in the row.
  • pinpoint_unavailable: the decision exists; the numbered passage is not in the structure index. For a lettered sub-number (E. 2a, E. 3c/aa) the parent number is returned (parent_retrieved); locate the letter inside it.
  • missing: no decision with that citation or docket. A wrong citation or a gap in coverage, never proof that a citation was invented.
  • ambiguous: more than one decision carries that label (dockets are reused across courts). Pick a decision_id.
  • unrecognized: the decision the service proposed carries no label written in the reference; the proposal is under service_candidate, never in decision_id. resolution_incomplete, error: identity could not be established, or the request failed. Nothing is guessed.
  • total_is_lower_bound: true means “at least this many”. A text query is ranked over a bounded candidate pool, so has_more: false never proves that every relevant decision was seen.
  • Exit codes: 0 complete, 2 invalid input, 3 service or network failure, 4 partial or unresolved (including a decision or passage the service does not have). Scripts branch on them without parsing prose.
  • discrepancy: the decision was identified, but the date or the docket written next to the BGE label contradicts the record; discrepancies lists each one.

Recipes

Search, then fetch the full decisions, in one pipeline:

ocl decisions search 'Rachekündigung Art. 336 OR' --max-results 5 --format jsonl |
  ocl decisions get --stdin --format jsonl > decisions.jsonl

Everything a court decided in a period. Without query text the filters enumerate an exact set, page by page:

ocl decisions search --court bge --date-from 2026-01-01 --date-to 2026-06-30 \
  --sort date_desc --max-results 500 --format jsonl > bge-2026-h1.jsonl

Quote one Erwägung verbatim, and get the citation to go with it:

ocl decisions passage bge_BGE_136_III_513 2.3
ocl cite 'BGE 136 III 513' --pinpoint 2.3 --language fr

A statute as it stood on a date:

ocl laws get OR --article 41 --as-of 2015-01-01

Who cites a leading case:

ocl citations list bge_BGE_140_III_86 --direction incoming --limit 50

Coding agents such as Claude Code or Codex run the same commands from a shell and read the JSON, which keeps the evidence in files you can check rather than in the model's memory.

For agents

An agent needs four things: an install without prompts, output it can parse, verdicts it can branch on, and rules it cannot talk itself out of. ocl provides all four: JSON as soon as output is piped; exit codes that carry the verdict (0 all resolved, 2 invalid input, 3 service or network, 4 something did not resolve); --cache so repeats within a database generation cost nothing; and three bundled skills (citation check, research, evidence bundle). ocl tool call reaches every research tool of the service.

ocl agent-guide                  # the contract on one page: JSON, exit codes, statuses, rules
ocl skills install --claude      # citation-check, research, evidence-bundle into ~/.claude/skills
ocl tool list                    # every research tool of the service
ocl tool call find_leading_cases query='Rachekündigung Art. 336 OR' limit=5
ocl quotes check --input quotes.jsonl --format jsonl
ocl citations resolve --input refs.jsonl --format jsonl --cache ~/.cache/ocl

The rules are not negotiable: citation strings and quotations come from the service unchanged; a close_match or service_candidate is information for the author, never a substitute; the tool establishes existence and wording, not legal weight. opencaselaw_cli.api offers the same as a library.

Where it fits

  • Conversation: connect Claude, ChatGPT or another MCP client to mcp.opencaselaw.ch (setup per client).
  • Scripts, notebooks and agents: ocl, or the REST API directly. Same records, same limits.
  • Corpus-scale analysis: the Parquet dataset.

Limits

A text query is sent as one ranked request of at most 800 results; filter-only searches are paged. Broad queries can exceed the timeout and are reported as a failure, not as an empty result. Cantonal statutes are addressed as --law ZH/StG:1. Queries go to the hosted service and are subject to the privacy notice; --base-url points at a separately operated server.

The full guide with the reference sections (search and retrieval, bundles, citation resolution, the research API contract) is on GitHub: docs/research-cli.md.