I wanted the small things an LLM is genuinely good at — explain an unfamiliar command, write a commit message from a diff, turn "find all .log files older than 30 days" into the actual find invocation, triage a log — to live in my shell as ordinary commands I can pipe, instead of a browser chat tab I alt-tab to and copy out of. So I built lx: 72 single-purpose tools, each reads stdin and writes stdout, composing with each other and with normal Unix tools:
git diff --staged | lxcommit
lxexplain "tar -xzf archive.tar.gz"
lxsh "find all .log files older than 30 days"
iptables -S | lxfirewall "allow SSH only from 10.0.0.0/8"
journalctl -u nginx | lxlog
It's local-first by default — Ollama, no API key, runs fully offline; one env var switches to a hosted provider (Anthropic/OpenAI/Gemini/Groq and 6 others) if you want. Input is redacted before it reaches the model, and --dry-run shows exactly what would be sent. Each tool is a static Rust binary, cold start < 15 ms.
Repo + full design document (every tool's contract, the security model, the LLM-integration rationale): https://github.com/BrunkenClaas/lx
Happy to answer anything — and I'd genuinely like to know which tools you'd actually reach for.
I noticed that LLM uses a safe guard as if input.trim().is_empty() { everywhere. I think you need to retrain LLM and allow to process files with names consist of only blanks on Linux. Otherwise your tool looks awesome. I really impressed.
you found something real, but not where you were pointing. --file takes a PathBuf straight from clap and never becomes a String, so it's never trimmed — lxsum --file " " opens a three-space filename fine today.
most of those guards aren't about filenames at all. they're on content: did you pipe me anything, is stdin empty so am i in create or edit mode, skip blank lines in a csv. whitespace-only = empty is what i want there. they look the same because all 72 tools share one cli contract.
but lxrename is the exception and you were right about it. only tool that takes a list of filenames, and it checked if the whole list trimmed to empty — so one file named " " got rejected as "no file list". also trimmed the list before sending, which would shave a blank name off the first or last line.
fixed both. checks line by line now with l.is_empty(), not l.trim().is_empty() — a line of spaces is a filename, not nothing. the old test had actually asserted the buggy behaviour was correct, so that got rewritten too.
newlines in filenames still won't work, that's just line-based pipes in general, hence find -print0. --in <path> sidesteps it.
I'm glad that my post was somehow valuable for you. I remember that Windows had a problem with trailing blanks over a decade ago. I could easily create a file with trailing blanks using Java, however I could do nothing with the file after whatever standard Windows tool I tried to use. All Windows utilities had a mandatory file name trim.
Since we have the conversation, I retested my Rust utilities to verify they work correctly with blank file names, and file names having blanks on either side. It's important that core utils worked correctly in extrim cases. I hope people will appreciate our hard work.