krino
krino sorts a directory by rules you write. It reads the files, works out what belongs where, shows you the plan, and touches nothing until you say so. Every run is logged, and any run can be undone.
Rules are conditions — a file's type, name, path, size, age, text content,
or whether it is a duplicate of something you already have — combined with
and, or and not, and actions: move, copy, rename, delete. There is a
command line and an optional GTK 4 window onto the same engine.

A rules file
One file per directory, in ~/.config/krino/dirs/:
(path "~/Downloads")
;; Never touch these, whatever the rules below say.
(exclude (name "^keep-"))
(rule "invoices"
(when (type document)
(content "invoice" "faktura"))
(move "Filed/Invoices/{mtime:%Y}")
(stop))
(rule "screenshots"
(when (and (type image) (name "^screenshot-([0-9-]+)")))
(rename "screen {1}.png")
(move "Filed/Pictures")
(stop))
(rule "copies I already have"
(when (duplicate "~/Documents"))
(move "~/.dupes")
(stop))
(rule "old installers"
(when (and (type iso exe) (age > 90d)))
(delete))
Conditions in a (when ...) must all hold; a (content ...) is true when
any of its keywords appears. {mtime:%Y} and {1} are placeholders — the
file's year, and the first capture group of the name test. (stop) means a
file this rule matched takes no later rule. Four worked files are in
examples/, and krino.conf(5) is the reference for every form.
What it can do
- Conditions:
type(by extension or by kind —image,document,archive),nameandpath(regular expressions, with capture groups),size,age,content(text pulled out of PDFs and office documents),duplicate(the same bytes as a file somewhere else), andmatched(what an earlier rule decided). Combined withand,or,not. - Actions, chained in the order written:
move,copy,rename,delete. Placeholders fill in the file's name, its date, or a capture group from the rule that matched it. - A plan first.
-nprints what would happen and changes nothing. Without it krino asks — all of it, or file by file, one key each. - Undo.
krino undoreverses the last run, or one named by id. A reversal the world has moved on from is refused for that file, not guessed at. - A log. Every step of every run, tab-separated, in
~/.local/state/krino/krino.log. - Fail-closed. A PDF whose text cannot be read does not match a content test, and so is left alone rather than filed by a guess.
- Fast on a rerun. Extracted keywords are cached, so a second pass over a large directory does not re-read every document.
It is not a watch mode, OCR, EXIF dates, a shell-command action, a way to look inside archives, or macOS and Windows support.
Install
make && make install
make install puts the binary in $PREFIX/bin (default ~/.local/bin),
the man pages in $PREFIX/share/man, and the examples and a short
s-expression tutorial in $PREFIX/share/doc/krino. On OpenBSD, whose Go
package defaults to GOTOOLCHAIN=local, run GOTOOLCHAIN=auto make.
Some (content ...) tests need external extractors — pdftotext for PDF,
antiword or catdoc for legacy Word, xls2csv and catppt for legacy
Excel and PowerPoint. make build says which are missing, and make deps
installs them with the system package manager. That is the only target that
asks for privileges; plain make never does.
Try it in a scratch directory
mkdir -p /tmp/krino-demo && cd /tmp/krino-demo
for f in acme-invoice.pdf photo.jpg podcast.mp3 notes.txt; do
printf 'demo file: %s\n' "$f" >"$f"
done
export XDG_CONFIG_HOME=/tmp/krino-xdg/config
export XDG_STATE_HOME=/tmp/krino-xdg/state
export XDG_DATA_HOME=/tmp/krino-xdg/data
krino init
krino new demo /tmp/krino-demo
cat > "$XDG_CONFIG_HOME/krino/dirs/demo.conf" <<'EOF'
(path "/tmp/krino-demo")
(min-age 0s) ; these files are seconds old; the built-in is 2m
(rule "acme"
(when (name "acme"))
(move "Filed/Acme"))
EOF
krino -n demo prints the plan and changes nothing:
krino: demo /tmp/krino-demo
4 scanned · 1 to act on · 0 warnings · 0.00s
1 acme-invoice.pdf
move → Filed/Acme/
rule acme
because name "acme"
not acted on: 3 unmatched (-v lists them)
Without -n krino shows that plan and asks — [a] apply all, [c] choose
per file, [s] skip this directory, [q] quit — and under c, each file
in turn, where t sends it to the Trash and d deletes it instead of what
the rules planned. -y applies without asking, which is what a README can
honestly paste:
$ krino -y demo # the plan above, and then:
1 applied · 0 failed · 0 declined
$ krino log
20260917T115059-6df2 2026-09-17 11:50 demo 1 moved
$ krino undo -y
krino: undo 20260917T115059-6df2
1 files · 1 to reverse · 0 refused
# file steps
1 demo/acme-invoice.pdf undo-move → /tmp/krino-demo/acme-invoice.pdf
undo-mkdir /tmp/krino-demo/Filed/Acme
undo-mkdir /tmp/krino-demo/Filed
1 applied · 0 failed · 0 declined
/tmp/krino-demo is back as it was. Clean up with
rm -rf /tmp/krino-demo /tmp/krino-xdg.
The window
make gui && make install-gui
krino-gui plans a directory, shows what would happen to each file with
the reason beside it and the file itself below, applies the ones you check,
undoes a run, and edits a directory's rules as forms or as text with the
configuration checked as you type. Every decision is the engine's, so the
window and the command line agree, and a run it applies is an ordinary run
that krino undo can reverse.
It lives in gui/, a nested module, and needs GTK 4 development files and
cgo — which the command line does not, so neither is needed to build or use
krino itself:
sudo apt install libgtk-4-dev libgirepository1.0-dev # Debian, Devuan
Safety
(delete)moves a file to the freedesktop.org Trash, notunlink(2).(delete permanent)is the explicit opt-out, and undo can never reverse it.- Duplicates are found, never deleted. A rule may combine
(duplicate)withmovebut never withdelete, and no other rule may delete a file krino has found to be a duplicate. Move copies aside and delete them yourself, or with a tool such as jdupes. - A directory is locked while krino works on it, so two runs cannot fight over the same files.
- Your rules live in
~/.config/krino, never in this repository. A leak check inmake cirefuses staged files holding a home directory path or an email address, and, withgit config krino.leakpatterns FILE, any pattern of your own.
Status
0.0.12.
go install git.labunix.xyz/krino/cmd/krino@latest
git clone https://git.labunix.xyz/krino.git # or build from a clone
The shape of krino -n --json is unstable before 1.0 — don't script
against it yet.
Builds and tests are run on Linux, FreeBSD and OpenBSD. The release tarballs hold the cross-compiled command line only; the window is built on the machine that runs it.
Licence
GPL-3.0-or-later, in full in LICENSE. By Lukasz Kasprzak —
lukas@labunix.xyz.
