A tiny CLI wrapper around claude -p that turns a natural-language request into a ready-to-run shell command — and drops it straight into your zsh edit buffer so you only hit Enter.
$ cl "list the 5 largest files in ~/Downloads"
📋 copied to clipboard
$ du -ah ~/Downloads | sort -rh | head -5
The second line above is the generated command, pre-filled on your prompt by zsh's print -z. You can inspect, edit, or just hit Enter.
claude is great for answering questions, but when you just need one shell command, you don't want a conversation — you want the command, context-aware for your actual machine (macOS vs Linux, BSD vs GNU coreutils, installed tools, current directory), and you want it ready to run.
That's all cl does.
Requires: bash, zsh, claude CLI (from Claude Code), jq. Optional: shellcheck, pbcopy (macOS default), homebrew coreutils/gnu-sed for GNU tools.
git clone https://github.com/<you>/claude-cl ~/src/claude-cl
cd ~/src/claude-cl
./install.shThe installer symlinks cl into ~/.local/bin/ and prints the one line to add to ~/.zshrc:
# claude-cl: source cl.zsh from wherever the symlink in ~/.local/bin points
[[ -L ~/.local/bin/cl ]] && source "${$(readlink ~/.local/bin/cl):h}/cl.zsh"The line resolves the symlink, takes its dirname via zsh's :h modifier, and sources cl.zsh from there. Move or rename the repo → just re-run ./install.sh and the zshrc line keeps working; no path baked in.
Reload: exec zsh.
cl "<natural language request>" # args
echo "<request>" | cl # stdin
cl # interactive prompt — just type/paste
cl --explain "<request>" # + one-line explanation
cl --dry-run "<request>" # skip clipboard
cl --model sonnet "<request>" # override default model (haiku)
cl -v "<request>" # print the prompt sent to the model
cl --help# everyday
cl "list the 5 largest files in ~/Downloads"
cl "kill whatever process is listening on port 3000"
cl "show disk usage of this dir, sorted, human-readable, top 10"
cl "find files changed in the last 2 hours under this dir"
# text wrangling (picks BSD vs GNU sed for you)
cl "replace foo with bar in all .ts files under src"
cl "lowercase every filename in ./assets"
cl "pretty-print the JSON in response.json in place"
# git
cl "list commits on this branch not yet on main"
cl "undo the last commit but keep the changes staged"
cl --dry-run "force push this branch to origin" # destructive guard fires
# docker / kubectl (only if installed — it checks)
cl "tail logs for the pod named api in the staging namespace"
cl "remove all stopped docker containers and dangling images"
# piped input
echo "convert every .heic in ~/Pictures to .jpg" | cl
pbpaste | cl # feed clipboard contents in
# with an explanation, or a different model
cl --explain "tail the last 100 lines across all .log files in ./logs"
cl --model sonnet "rebase the last 5 commits interactively onto main"
# see exactly what got sent to the model
cl -v "anything"cl prompts # list available prompt variants and their sizes
cl prompt <name> # print the contents of a prompt variant (default|medium|small|tiny|…)
cl models # list known model aliases
cl env # print the env context block cl sends to the model
cl refresh # clear tool-version cache + purge any /resume artifactsGenerate commands for a machine that isn't the one you're on — e.g. you're on macOS, want to paste a command into a Linux ssh session:
# I'm on macOS, want a Linux-safe command
cl --linux "tail the last 100 lines of all .log files modified today"
# → uses GNU find/sed/date/stat syntax
# I'm on Linux, need a command for a colleague's Mac
cl --macos "replace foo with bar in every .ts file here"
# → sed -i '' 's/…/…/' (BSD form)
# I don't know where this will run, make it bulletproof
cl --posix "list top 5 largest files in this dir"
# → no sed -E, no stat -c, no [[ ]], no process substitutionWhen a target is set, cl env reflects it — you can inspect exactly what context the model is getting:
cl --linux env
# ⚠ TARGET OVERRIDE — generate a command for Linux, NOT the user's current machine.
# target: linux
# target_userland: GNU …Target mode drops installed_tools, gnu_tools, tool_versions, and aliases_shadowing_binaries from the env block — those describe your machine, not the target. pwd is kept because "here" is usually still meaningful (e.g. you're about to scp the command up to a server running in the same dir structure). Layers cleanly with --alts, wtf, and --polish.
$ cl --alts 3 "find all images modified in the last 7 days under ~/Pictures"
alternatives:
1) find ~/Pictures -type f \( -iname '*.jpg' -o -iname '*.png' -o -iname '*.heic' \) -mtime -7
└─ portable BSD find, no extra deps
2) fd -t f -e jpg -e png -e heic --changed-within 7d . ~/Pictures
└─ fd (faster, brew install if missing)
3) mdfind -onlyin ~/Pictures 'kMDItemContentTypeTree == "public.image" && kMDItemFSContentChangeDate > $time.today(-7)'
└─ Spotlight metadata (instant, indexes only)
cl alts > █ ← fzf picker; Enter picks, Esc uses #1fzf is required for the interactive picker (you have it). Without fzf, option 1 lands in the buffer and the others stay listed for you to copy. Auto-fix is skipped in alts mode — you picked deliberately, the tool shouldn't silently swap your choice. Defaults to sonnet since picking-N-different-approaches benefits from reasoning.
Every generated command is run through bash -n before delivery. It's fast, silent on success, and catches the class of bugs that actually matter — unmatched quotes, busted heredocs, syntax errors (the typical failure mode is a JSON \n decoding into a real newline inside a quoted awk/sed format string). When it fails, cl automatically does a second pass with sonnet using prompts/fix.md:
✗ command does not parse:
line 2: unexpected EOF while looking for matching `''
parse error — asking sonnet for a fix...
was: awk '{printf "%s
", $1}' file
→ Consolidated the awk script onto one line and escaped the literal \n.
Shellcheck is opt-in now. Default runs don't show lint output — most findings (SC2010 ls | grep, SC2012 use find) are style nits, not real bugs, and the noise on every run was counterproductive.
--lint— run shellcheck, show findings, but don't retry.--polish— like--lint, plus retry with sonnet on any finding. The fix prompt is told to leave the command alone if the lint suggestion conflicts with your stated intent (e.g. you said "using ls" — it'll keepls).--no-fix— disable the parse-check-driven retry too.
Skipped automatically inside cl wtf (the wtf model already reasoned) and cl --alts (you picked deliberately, don't silently swap).
$ tar -czf out.tgz nonexistent/
tar: nonexistent: Cannot stat: No such file or directory
$ cl wtf
💭 The path "nonexistent/" doesn't exist in this directory, so tar can't stat it. Verify with `ls`.
$ ls -la nonexistent # ← pre-filled in your edit bufferWhat it sees:
- Always: the failed command (
fc -ln -1), its exit code (captured before anything else clobbers$?), your$PWD. - With
cl wtf --rerun: re-executes the command and captures its stdout+stderr. Refuses to rerun anything matching the destructive-pattern guard. - With
mycmd 2>&1 | cl wtf: pipes any text in as additional context.
Defaults to sonnet (better at reasoning) — override with CL_WTF_MODEL or --model. Uses prompts/wtf.md as its system prompt; tweak it like any other variant. Output: 💭 diagnosis to stderr, fixed command to stdout (gets print -z-injected like normal cl runs).
| flag | effect |
|---|---|
--explain / --no-explain |
Show (default) or suppress the explanation line. |
--dry-run |
Skip clipboard. (Buffer injection still happens.) |
--no-clipboard |
Skip pbcopy. |
--no-shellcheck |
Skip the parse check (bash -n) and any shellcheck pass. |
--no-fix |
Don't auto-retry on parse failure. (Default: retry if bash -n fails.) |
--lint |
Show shellcheck output (opt-in; no retry). Most shellcheck findings are style nits that aren't real bugs for a one-liner, so lint is off by default. |
--polish |
Like --lint, but also auto-retry with sonnet on any shellcheck finding. Adds ~3-5s when triggered. |
--alts [N] |
Return N (default 3, range 2–6) distinct alternatives using different tools/approaches. Picks via fzf if installed; otherwise injects #1 and lists the rest. |
--macos / --darwin / --linux / --posix / --target <name> |
Target a different flavor than the current machine. Replaces the env block with target-specific userland notes (BSD vs GNU gotchas, POSIX-only constraints). User-specific context (installed tools, aliases, tool versions) is dropped because the target may not have them. |
--no-minimal |
Disable minimal mode (let claude load hooks, user CLAUDE.md, auto-memory, plugins, MCPs). Minimal mode is on by default. |
--model <id> |
Override model. Default: $CL_MODEL or haiku. |
--prompt <name|path> |
Pick a prompt variant. Bare name → prompts/<name>.md; else file path. |
-v, --verbose |
Dump the full prompt + env block sent to the model. |
-h, --help |
Usage. |
--version |
Print version. |
CL_MODEL— default model alias or full id (e.g.haiku,sonnet,claude-opus-4-6).CL_PROMPT— default prompt variant (e.g.medium) or path.CL_MINIMAL— set to0to disable minimal mode globally (same as--no-minimal).CL_NO_FIX— set to1to disable shellcheck-error auto-fix globally.CL_FIX_MODEL— model for the auto-fix retry (default:sonnet).CL_WTF_MODEL— model used bycl wtf(default:sonnet).CL_ALTS_MODEL— model used bycl --alts(default:sonnet).
On by default. Appends these flags to every claude -p invocation so cl has no side effects on your session and can't be polluted by ambient config:
--setting-sources ''— no settings.json (no hooks, no auto-memory)--strict-mcp-config --mcp-config '{"mcpServers":{}}'— no MCPs, no plugin sync--disable-slash-commands— no skills resolutioncd /tmpbefore invoking — no project CLAUDE.md walk-up
Perf is roughly a wash vs. non-minimal (API variance dominates). The win is predictability: cl can't accidentally save a memory, fire a hook, or inherit your project's database config. OAuth subscription auth is preserved (unlike claude --bare).
Turn it off with --no-minimal or CL_MINIMAL=0 if you want your hooks/CLAUDE.md to apply.
clcollects a snapshot of your environment: OS + version, arch, shell,$PWD, homebrew prefix, which GNU coreutils are installed (gls,gsed, etc.), and which common tools are on PATH (git,jq,rg,docker,kubectl,gh,aws,psql, …).- It substitutes that env block into
prompt.md(the system prompt) and invokesclaude -p --model haiku --system-prompt <...> "<your request>". - The model returns JSON
{"command": "...", "explanation": "..."}. clruns a destructive-pattern guard (warns onrm -rf /, piped-curl-to-sh, force-pushes to main, etc. — never blocks).- If
shellcheckis installed, it lints the command and prints any issues dimmed on stderr. - The command is copied to your clipboard (unless
--dry-run) and printed on stdout. - The zsh function wrapper in
cl.zshcaptures stdout and callsprint -zto drop it into your edit buffer.
| stream | contents |
|---|---|
| stdout | the generated command, nothing else |
| stderr | clipboard note, destructive warnings, shellcheck output, --explain line, verbose dumps |
This separation is intentional — cl.zsh captures stdout only, so all the human-facing chatter appears above your newly-primed prompt line.
The system prompt lives in prompt.md. The placeholder {{ENV_CONTEXT}} is replaced at runtime with the env snapshot. Edit freely — no restart needed.
Useful places to start:
- Add project-specific context ("if the request mentions 'db', assume local postgres on port 5432").
- Tighten or loosen the destructive-intent guidance.
- Change the output style (e.g. require
set -euo pipefailprologues for scripts longer than one line).
- "cannot read prompt template" —
prompt.mdmust live next to the realclscript (the installer symlinks, so this keeps working). - Command has backticks or fences — the model occasionally wraps output in
```.clstrips these; if one slips through, add-vand inspect the raw response. claudeauth errors —clinherits yourclaudeCLI auth. Runclaudeinteractively once to authenticate.- Buffer injection not working — make sure you sourced
cl.zshin~/.zshrcAND reloaded your shell. Verify withtype cl— it should say "cl is a shell function". - Slow — default model is
haiku(fastest). If it still feels slow, the bottleneck is network RTT to the Anthropic API, not the wrapper.
- Non-zsh buffer injection (bash has no clean equivalent of
print -z). - Streaming output.
- Invocation history.
- Tests.
MIT. See LICENSE.