distill

Compile a folder of detailed per-rule markdown files into one short, AI-targeted markdown file. Uses claude --print to compress each section into bullet form; the output is regenerated end-to-end every run.
Built for CLAUDE.md generation. Source files stay human-friendly (one rule per file, with rationale, examples, anti-patterns). The output is dense, scannable, token-cheap — loaded into every AI agent session, derived, never edited by hand.
Install
go install github.com/bborbe/distill@latest
Requires the claude CLI on $PATH.
Usage
distill --source <rule-folder> --output <claude-md-file> --title "Global Preferences"
| Flag |
Required |
Meaning |
--source <dir> |
yes |
Folder containing source rule .md files |
--output <file> |
yes |
Output markdown file path (overwritten every run) |
--title <text> |
no |
Top-level # <text> heading written under the auto-generated warning |
--model <name> |
no |
Claude model name (default: sonnet) |
--verbose |
no |
Print per-section prompt + response to stderr |
Exit codes: 0 ok · 1 failure (parse / Claude / IO) · 2 usage error.
Each rule is a markdown file with distill: frontmatter declaring which section it belongs to, plus a long-form body that the LLM compresses:
---
distill:
section: Git
order: 10
id: no-git-c-flag
---
# Rule: No `git -C` Flag
Never use `git -C /path …` or `cd /path && git …` — both break the Bash
auto-approval matcher. The fix is to `cd /path` in its own Bash call, then
run `git status` / `git diff` in separate calls.
| Frontmatter field |
Required |
Meaning |
section |
yes |
Becomes the ## <section> heading the bullet lives under |
order |
no |
Sort key within section (default 100) |
id |
no |
Stable identifier; defaults to filename stem |
disabled |
no |
Skip emission when true |
Files without a distill: frontmatter block are silently skipped (they're treated as docs in the source folder).
Output shape
distill writes the file from scratch every run:
<!--
AUTO-GENERATED by distill — do not edit by hand.
Source: <absolute source dir>
Regenerate: distill --source <source> --output <output>
-->
# <title>
## Git
- **No Git -C Flag.** Never `git -C /path` or `cd /path && git ...`; first `cd /path` in its own Bash call, then `git` in separate calls.
## Operational
- **English Only.** Reply in English even when the user writes German; no code-switching.
- ...
Sections are ordered by minimum order within the section, then alphabetically. Bullets within a section sort by (order, id).
The output file is owned end-to-end by distill. Don't hand-edit it — your edits will be lost on the next regeneration. Edit the source rule files instead, then re-run.
Make integration
A typical setup with two make generate targets — one for global ~/.claude/CLAUDE.md, one for vault ~/Documents/Obsidian/Personal/CLAUDE.md:
# ~/.claude/Makefile
SOURCE := $(HOME)/.claude/claude-md-rules
OUTPUT := $(HOME)/.claude/CLAUDE.md
TITLE := Global Preferences
.PHONY: generate
generate:
@distill --source "$(SOURCE)" --output "$(OUTPUT)" --title "$(TITLE)"
@echo "wrote $(OUTPUT)"
cd ~/.claude && make generate
Non-goals (v1)
- No caching. Every run calls Claude. Re-running may produce slightly different phrasing per bullet (LLM is non-deterministic).
- No
--check mode. (Cannot exist without cache.)
- No watch / daemon mode. One-shot CLI.
- No marker-based partial-file addressing —
distill owns the whole output file.
- No multi-target broadcasting from a single source file.
Development
make precommit # lint + format + generate + test + checks
make test # tests only
See docs/spec.md for the full contract and docs/dod.md for the Definition of Done.
License
BSD-style — see LICENSE.