Most commands can be filtered with a one-liner in .lowfat (see CONFIG.md). Write a plugin only when pipeline config can't express what you need:
- Per-subcommand logic — different subcommands produce completely different output
- Conditional output — show "ok" when clean, show errors when not
- Context-aware filtering — different behavior based on flags or arguments
A plugin lives at ~/.lowfat/plugins/<category>/<name>/ and ships in one of two formats:
| Format | When to use |
|---|---|
filter.lf |
Default for new plugins. Declarative lf-filter rules parsed in-process; shell + python escape hatches for the rare cases that can't be expressed in built-in ops. |
filter.sh |
Legacy. POSIX shell script reading stdin → writing stdout. Still supported — when both files exist filter.lf is auto-detected; set runtime.entry to force filter.sh. |
lowfat plugin new kubectl # scaffold at ~/.lowfat/plugins/kubectl/kubectl-compact/This creates:
~/.lowfat/plugins/kubectl/kubectl-compact/
lowfat.toml # manifest
filter.lf # rule file (edit this)
samples/ # paste real output here for benchmarking
Edit the manifest:
[plugin]
name = "kubectl-compact"
commands = ["kubectl"]
subcommands = ["get", "describe", "logs", "apply"]commands— top-level command(s) intercepted (e.g.,kubectl); list shorthand aliases here too (e.g.,k)subcommands— which subcommands this plugin handles (omit to handle all)bin— optional real binary to exec when invoked via an alias; e.g.commands = ["kubectl", "k"]+bin = "kubectl"makeslowfat k …runkubectleven thoughkis only a shell alias- the entrypoint is auto-detected (
filter.lf, elsefilter.sh) — add a[runtime]table only to override it or declarerequires
lf-filter is lowfat's declarative plugin DSL. A .lf file is a sequence of include directives, define blocks, and rule blocks; the runner picks the first rule whose selector matches (subcommand, level) and runs its ops top-to-bottom.
status: # one subcommand
build|check: # subcommand alternation
apply*: # `*` glob inside the token
*: # catch-all (put last)First match wins. Order matters: put specific rules before catch-alls. To branch on level, exit code, or flags, use a cascade in the rule body — not the selector.
| Op | Form | What it does |
|---|---|---|
keep |
keep /regex/ |
Keep lines matching the regex |
drop |
drop /regex/ |
Drop lines matching the regex |
head |
head N or head auto |
First N lines (auto = level-scaled: 15/30/60 for ultra/full/lite) |
tail |
tail N or tail auto |
Last N lines |
or |
or "text" |
If state is empty, emit literal text (else = legacy alias) |
or-shell: |
or-shell: <cmd> |
If state is empty, run <cmd> with the original raw input |
raw |
raw |
Emit the stream unchanged — the conservative arm of a cascade (passthrough = legacy alias) |
split |
split /regex/ + pre: / post: blocks |
Split input at first matching line, run separate chains on each half |
Regex uses /.../ delimiters (escape / as \/). Built-in ops are pure Rust — no subprocess overhead.
Ops run top-to-bottom as a pipeline — each one receives the previous op's output. Combinations compose by intersection: keep /error/ then drop /ignored/ keeps lines matching error AND not matching ignored. Order matters: keep /X/ then drop /X/ produces nothing.
A rule body is either a pipeline (the ops above) or a cascade: if / elif / else arms where the first matching guard wins and exactly one arm runs.
diff:
if exit failed: raw
elif level ultra: compact-diff 30
elif --stat: compact-diff 40
else: compact-diff 200An arm body is itself a pipeline — inline after the colon, or indented for several ops. A body is a pipeline or a cascade, never both.
Guards name a closed set of dimensions — no expressions, no comparisons:
| Guard | True when |
|---|---|
exit failed / exit ok |
command exit code ≠ 0 / = 0 |
level ultra|full|lite |
active level |
--stat, -p, … |
that flag is present in $args |
Join guards with and — if level ultra and --stat:. For "or", write separate arms: first-match-wins is the disjunction. else is optional; if no arm matches, the stream passes through unchanged.
| Op | Form | Notes |
|---|---|---|
shell: |
shell: <inline> or shell: | + indented block |
Runs under sh -c. Env: $level, $sub, $args, $exit |
python: |
python: | + indented block |
python3 -c for plain bodies; uv run --script for bodies with a # /// script PEP 723 header |
Reusable op sequences with positional arguments:
define strip-trailers:
drop /^[[:space:]]*(Signed-off-by|Co-authored-by):/
define compact-diff(limit):
shell: |
awk -v lim=$1 -v lvl=$level '
BEGIN { in_hunk=0; n=0 }
n>=lim { exit }
/^diff / { in_hunk=0; print; n++; next }
/^@@ / { in_hunk=1; print; n++; next }
in_hunk && /^[+-]/ { print; n++ }
'$1, $2, … are substituted at execution time. $level, $sub, $args, $exit are left for the shell to expand from env vars.
Invocation:
diff, ultra: compact-diff 30
diff, lite: compact-diff 400
diff: compact-diff 200
strip-trailers # zero-arg callPull another file's defines into the current file so wrappers can reuse a
tool's compaction instead of copying it:
include lib/pytest.lf # path is relative to THIS file
*:
compact-pytest 45 # macro defined in lib/pytest.lf- Macros only. Rules in an included file are ignored, so a real filter can also serve as a library.
- Path is relative to the including file; transitive includes work.
- Override by redefining. A local
definewith the same name shadows the imported one — handy to tweak a single inherited macro. - Collisions error. The same name imported from two different files is a hard error (rename one, or override it locally). Cycles are rejected.
- Only files loaded from disk can
include— embedded/bundled filters can't.
Runnable example: examples/include.
diff: compact-diff 30 or-shell: awk 'NF' | head -50shell: / python: / or-shell: greedily consume the rest of the line.
#!/usr/bin/env lowfat-filter
define strip-trailers:
drop /^[[:space:]]*(Signed-off-by|Co-authored-by|Change-Id|Reviewed-by|Acked-by|Tested-by|Reported-by|Cc):/
define abbrev-hash:
shell: sed -E 's/^commit ([0-9a-f]{12})[0-9a-f]{28}/commit \1/'
define compact-diff(limit):
shell: |
awk -v lim=$1 -v lvl=$level '
BEGIN { in_hunk=0; n=0; xf=0; xl=0 }
/^diff / { in_hunk=0
if (n>=lim) { xf++; next }
print; n++; next }
/^@@ / { in_hunk=1
if (n>=lim) { xl++; next }
if (lvl=="ultra" && match($0,/ @@/))
print substr($0,1,RSTART+2)
else print
n++; next }
lvl=="ultra" { next }
in_hunk && /^[+-]/ { if (n>=lim) { xl++; next }
print; n++ }
END { if (xf || xl)
printf "... [git-compact: truncated; %d more files, %d more lines omitted - use LOWFAT_LEVEL=lite for the full diff]\n", xf, xl }
'
define drop-index-meta:
drop /^(index [0-9a-f]+\.\.[0-9a-f]+( [0-7]+)?$|(new file mode |deleted file mode |old mode |new mode |similarity index |dissimilarity index |rename from |rename to |copy from |copy to |---|\+\+\+) )/
status:
keep /^\t/ # long-format file entries are tab-indented
head 30
or "git status: clean"
diff:
if exit failed:
raw
elif level ultra:
compact-diff 30
or-shell: awk 'NF' | head -50
elif level lite:
drop-index-meta # escape hatch: no caps, only redundant
drop /^ ?[[:space:]]*$/ # meta + blank context lines dropped
else:
compact-diff 200
or-shell: awk 'NF' | head -50
log:
if level ultra:
keep /^(commit | )/
strip-trailers
abbrev-hash
head 10
else:
strip-trailers
abbrev-hash
head 25
show:
if level ultra:
keep /^(commit |Author:|Date:| |diff --git)/
strip-trailers
abbrev-hash
head 20
else:
split /^diff /
pre:
keep /^(commit |Merge:|Author:|Date:| )/
strip-trailers
abbrev-hash
post:
compact-diff 100
head 100
*:
head 30The cascades branch on exit code and level declaratively; the shell escape hatches handle the genuinely stateful work (diff hunk machine, sed rewrite). A git diff that exits non-zero passes straight through — conservative by default, no shell: block needed.
kubectl get -o yaml dumps server-side fields (managedFields, resourceVersion, generation, creationTimestamp) that drown the manifest. Real YAML parsing beats regex — annotations can contain anything including embedded ---.
#!/usr/bin/env lowfat-filter
define clean-yaml:
python: |
# /// script
# requires-python = ">=3.10"
# dependencies = ["pyyaml>=6"]
# ///
import sys, yaml
DROP = {"managedFields", "resourceVersion", "generation",
"creationTimestamp", "uid", "selfLink",
"ownerReferences"}
def prune(node):
if isinstance(node, dict):
if "annotations" in node:
node["annotations"] = f"<{len(node['annotations'])} entries>"
return {k: prune(v) for k, v in node.items() if k not in DROP}
if isinstance(node, list):
return [prune(x) for x in node]
return node
raw = sys.stdin.read()
try:
docs = list(yaml.safe_load_all(raw))
except yaml.YAMLError:
sys.stdout.write(raw) # passthrough non-YAML
sys.exit(0)
for d in docs:
if d is None: continue
yaml.safe_dump(prune(d), sys.stdout, default_flow_style=False, sort_keys=False)
print("---")
get:
clean-yaml
head 200
logs:
if level ultra:
keep /ERROR|FATAL|panic|Exception/
tail 30
or "no errors in window"
else:
drop /^\s*$/
tail 60
events:
if level ultra:
keep /Warning|Error/
tail 20
else:
tail 40
*:
head 30Declare uv as a dep in lowfat.toml:
[runtime.requires]
python = ">=3.10"
uv = "*"lowfat plugin doctor detects the # /// script header and prewarms the uv env (~2 s first time, then cached). Subsequent runs hit a warm cache (~200 ms overhead). Use Python for heavy commands (kubectl describe, terraform plan); stay with shell/awk for hot-path commands (git status).
| Level | What to emit |
|---|---|
ultra |
Verdict line(s) only — what the AI needs to decide next |
full |
Strip progress chatter / banner prose; keep diffs / errors / structure |
lite |
Gentle trim, higher row caps |
$exit != 0 |
Be conservative — preserve error blocks |
$sub is the first arg of the original command (get, describe, …). Walk $args when the subcommand alone isn't enough (resource type, output flags). Empty output = passthrough (lowfat falls back to the original).
# Run a sample through the filter
cat samples/git-diff-full.txt | lowfat filter ./filter.lf --sub=diff --level=ultra
# Inspect per-stage cost (line/byte/token counts to stderr; output still to stdout)
cat samples/git-diff-full.txt | lowfat filter --explain ./filter.lf --sub=diff --level=ultra
# Exercise an `if exit failed:` arm — --exit sets $exit / the exit guard
cat samples/git-diff-fail.txt | lowfat filter ./filter.lf --sub=diff --exit=1
# Side-by-side comparison
diff -u \
<(lowfat filter ./filter.lf --sub=diff --level=full < raw.diff) \
<(LOWFAT_LEVEL=full LOWFAT_SUBCOMMAND=diff sh ./filter.sh < raw.diff)# Naming convention: <command>-<subcommand>-<level>.txt
kubectl get pods -A > samples/kubectl-get-full.txt
kubectl describe pod mypod > samples/kubectl-describe-full.txt
lowfat plugin bench kubectl-compactAim for 80%+ savings at full on noisy commands, while keeping all actionable information.
lowfat plugin doctor
# checks: manifest parses, .lf parses, uv installed if needed,
# pre-resolves PEP 723 envs so first real run is fastThe script reads stdin, writes stdout, and gets these env vars:
| Env var | Value | Example (lowfat kubectl get pods -n kube-system) |
|---|---|---|
$LOWFAT_COMMAND |
top-level command | kubectl |
$LOWFAT_SUBCOMMAND |
first argument | get |
$LOWFAT_ARGS |
all arguments joined | get pods -n kube-system |
$LOWFAT_LEVEL |
lite / full / ultra |
full |
$LOWFAT_EXIT_CODE |
command's exit code | 0 |
The recurring shape:
#!/bin/sh
RAW=$(cat)
LEVEL="${LOWFAT_LEVEL:-full}"
SUB="${LOWFAT_SUBCOMMAND}"
case "$SUB" in
<subcommand>)
if [ "$LEVEL" = "ultra" ]; then
# Extract summary/errors only
else
LIMIT=$( [ "$LEVEL" = "lite" ] && echo 60 || echo 30 )
echo "$RAW" | grep -vE '<noise patterns>' | head -n "$LIMIT"
fi
;;
*)
echo "$RAW" | head -n 30
;;
esacMix your plugin with built-in processors in .lowfat:
pipeline.kubectl = strip-ansi | kubectl-compact | truncate:100
pipeline.kubectl.error = strip-ansi | head
[plugin]
name = "kubectl-compact"
version = "0.1.0"
description = "Compact kubectl output"
author = "you"
commands = ["kubectl", "k"] # list shorthand aliases too
subcommands = ["get", "describe", "logs", "apply"]
bin = "kubectl" # exec this binary even when invoked as the alias `k`
[runtime]
entry = "filter.lf" # optional — auto-detected (filter.lf, else filter.sh)
[runtime.requires] # checked by `lowfat plugin doctor`
python = ">=3.10"
uv = "*"
[hooks]
on_install = "chmod +x filter.sh"
[pipeline]
pre = ["strip-ansi"] # run before your filter
post = ["truncate"] # run after your filterCopy-paste this into Claude Code (or another tool-using agent) and replace <COMMAND>:
Create a lowfat plugin to filter `<COMMAND>` output for LLM contexts.
Before writing code:
1. Read docs/PLUGINS.md to learn lf-filter, lowfat's plugin DSL: ops
(keep/drop/head/tail/or/raw, shell:, python:), subcommand
selectors, if/elif/else cascades with exit/level/flag guards,
define macros, split.
2. Ask me: which subcommands to specialize, and what's noise vs. signal
in this command's output.
Scaffold at `~/.lowfat/plugins/<COMMAND>/<COMMAND>-compact/`:
- `lowfat.toml` — manifest with `commands` (any aliases too) and the agreed
`subcommands` list (no `[runtime]` needed — `filter.lf` is auto-detected)
- `filter.lf` — rules, top-down first-match; reach for `shell:` / `python:`
only when keep/drop/head can't express it
Filter contract:
- level=ultra → verdict line(s) only
- level=full → strip noise (progress chatter, banner prose), keep diffs /
errors / structure
- level=lite → gentle trim, higher row caps
- Non-zero $exit → be conservative; `if exit failed: raw`
- Use $sub, fall back to walking $args when you need flags or resource type
Verify:
- `lowfat plugin doctor` (parses cleanly; prewarms uv envs)
- Drop real captures in `samples/<command>-<sub>-full.txt`
- `cat samples/foo.txt | lowfat filter --explain filter.lf --sub=... --level=...`
- `lowfat plugin bench <name>` (aim ≥80% at full on noisy commands)
- Smoke-test: `lowfat <COMMAND> ...` against a real run
The agent will ask you what counts as noise — answer with sample output if you have it, then iterate on the bench numbers.