Fuzzy Quit (fuzzy-quit) is a small Bash tool that stops running software using graduated escalation (polite quit → stronger signals). It accepts exact names, paths, and unambiguous fuzzy substrings (with interactive disambiguation when needed).
macOS and Linux: Install and use the same quit command on both platforms. Linux (and other non-macOS Unix) focuses on command-line processes with the full SIGINT → SIGTERM → SIGKILL ladder. macOS adds extra capabilities on top of that: .app GUI applications, AppleScript / System Events where available, and richer matching against installed apps—details are in Requirements below.
The command you run is still named quit on your PATH; the repository and package name are fuzzy-quit so the project is easy to find on GitHub.
- Upstream: github.com/mlevin2/fuzzy-quit
- License: MIT — see
LICENSEin the repository root. - Contributing: see CONTRIBUTING.md.
- Installing (Homebrew keg, source, Docker checks): see INSTALL.md.
GitHub topics (for discoverability — GitHub allows 20 topics per repo; upstream is set with gh repo edit --add-topic …):
macos · bash · shell-script · zsh · fzf · killall · pgrep · process-management · cli · automation · applescript · macos-apps · linux · signals · sigterm · sigkill · productivity · dotfiles · substring-matching · fuzzy-matching
(Use Search keywords below for extra terms like “terminal” that do not fit the topic cap.)
Search keywords: quit applications, kill processes, graduated kill, macOS quit app, AppleScript quit, killall wrapper, process picker, fuzzy process name, interactive quit, fzf process selection.
- macOS — full behavior:
.appbundles, AppleScript, System Events (optional), substring app matching, and process escalation. - Linux (and other non-macOS Unix): processes only — same SIGINT → SIGTERM → SIGKILL ladder via
killall/pgrep. Noosascript, no.appintegration, no installed-GUI substring catalog (interactive list is mostlypsnames). A target that would be an “application” on macOS is handled with the process ladder after a short warning. - Bash
killallandpgreponPATH(on Debian/Ubuntu,psmisc/procpspackages)- Optional: fzf for interactive picking and fuzzy search
All logging and TUI helpers are vendored in lib/log.sh (no external dotfiles library).
Full guide (Homebrew keg layout, upgrade/uninstall, PATH with a dev checkout, Docker): INSTALL.md.
Homebrew (macOS or Linux):
brew install mlevin2/tap/fuzzy-quitFrom source:
git clone https://github.com/mlevin2/fuzzy-quit.git && cd fuzzy-quit && chmod +x quit
ln -sf "$(pwd)/quit" "$HOME/bin/quit" # or any directory on your PATH; symlinks are supportedConfirm: quit --version and quit --help.
Try the Homebrew formula in Docker (no host install): bash scripts/test-homebrew-docker.sh (see INSTALL.md).
quit [<options>] [<target>...]
quit [<options>] [--no-ps]
quit [<options>] --pick | -p [--no-ps]
| Option | Meaning |
|---|---|
-h, --help |
Usage (exits before processing targets if present). |
--version |
Print version from VERSION. |
-n, --dry-run |
Show how each target would be quit; no osascript or killall. |
--confirm-sigkill |
Prompt before the final killall -9 step. |
--no-ps |
Interactive mode: candidate list is apps only (no ps comm names). |
With no arguments, or only --pick / -p, quit opens fzf (multi-select with Tab) when available; otherwise it prompts for one target per line until a blank line.
Examples:
quit Safari node "/Applications/Slack.app"
quit --dry-run outlook
quit --confirm-sigkill SomeApp
quit --no-psExit status is 0 only if every target is handled successfully; otherwise 1.
Platform: uname -s must be Darwin for any macOS-only step below (bundle catalog, lsof .app detection, AppleScript, System Events, substring app matching).
For each target, in order:
- Bundle directory — Existing directory whose name ends in
.app. On macOS → application; elsewhere → process (basename without.app) for the signal ladder only. - Regular executable file — Existing non-
.appexecutable; process (basename). - Known install locations — macOS only:
<name>.appunder/Applications,~/Applications,/System/Applications,/Applications/Utilities(up to three levels deep). - Exact running process name —
pgrep -ix. macOS: iflsofshows a binary under a standard.app→ application; else process. Non-macOS: always process oncepgrepmatches (no.appwalk). - Substring on app names — macOS only (installed + running GUI names). Same disambiguation rules as before.
- Substring on
pscommnames — Any supported OS. - Otherwise — Process name for
killall(case frompgrepwhen possible).
Path-shaped arguments (containing /) skip substring steps; only the basename is used for the steps above.
On macOS, the first substring or interactive use in one run caches installed (and optionally GUI) app names.
| Variable | Meaning |
|---|---|
QUIT_INTERACTIVE_INCLUDE_PS |
0 (also set by --no-ps) omits ps names from the interactive list. |
QUIT_SKIP_SYSTEM_EVENTS |
1 skips AppleScript / System Events for running GUI names (tests, headless). |
QUIT_DRY_RUN |
1 — same idea as --dry-run (usually set by the driver). |
QUIT_CONFIRM_SIGKILL |
1 — same idea as --confirm-sigkill. |
If several apps or processes match a substring, quit does not guess: fzf or select on /dev/tty.
Application (macOS only): AppleScript quit → AppleScript quit saving no → killall (SIGTERM) → killall -9.
Application resolved on non-macOS: same as process (warning printed; no AppleScript).
Process: killall -INT → killall (SIGTERM) → killall -9.
Below, “app ladder” and “process ladder” refer to those sequences. Each argument is classified independently.
| Input | Behavior |
|---|---|
quit -h / quit --help |
Prints usage; exits 0. |
quit |
Interactive picker (fzf or tty). |
quit --pick / -p |
Same. |
quit --no-ps |
Interactive list without ps names (fzf only; ignored with tty fallback). |
quit Safari --no-ps |
Not interactive: --no-ps stripped; only Safari is processed. |
quit a b c |
Three targets, independently. |
quit --version |
Prints VERSION and exits. |
| Example | Typical behavior |
|---|---|
quit "/Applications/Safari.app" |
App → app ladder. |
quit "/opt/homebrew/bin/node" |
Process node → process ladder. |
quit Safari |
App if bundle found → app ladder. |
quit node (CLI running) |
Process node → process ladder (substring apps skipped). |
quit outlook |
Substring → one app (e.g. Microsoft Outlook) → app ladder. |
quit microsoft (ambiguous) |
Picker → one app → app ladder. |
quit nosuchthing_xyz |
Process name as given → process ladder (may warn if nothing runs). |
quit Foo/Bar |
Basename Bar; no substring steps. |
Pickers use framed headers (hr, bold titles, dim hints). fzf uses rounded borders and markers. Normal operation logs with colored info / warn / ok / err.
- First match for some exact bundle lookups (
find-quit); no prompt for duplicate exact names. - Apps only outside scanned trees and not running may need a full
.apppath. QUIT_SKIP_SYSTEM_EVENTS=1drops running GUI names from the substring merge.
GitHub Actions runs the Linux and macOS workflows on every push and pull request (see badges at the top of this file). You do not need Docker for CI—that is entirely on GitHub’s runners.
Locally: from the repository root (full suite on macOS; Linux runs all tests except test-case-insensitive.sh):
bash tests/run.shLint (requires shellcheck):
bash scripts/shellcheck.shtests/test-case-insensitive.sh sets QUIT_SKIP_SYSTEM_EVENTS=1 and runs only on macOS. On Linux, tests/run.sh skips it automatically. The runner discovers every tests/test-*.sh file.
Use this when you are on macOS (or Windows) and want the same shellcheck + test steps as the Linux workflow on your machine (Docker + Docker Compose v2). The GitHub workflow still runs on push/PR; this is optional local parity.
Recommended — from anywhere (script cds to the repo root):
bash scripts/test-linux-docker.shOr from the repository root:
make test-linuxSame checks, via Compose directly:
docker compose run --rm test-linuxThat runs scripts/shellcheck.sh then tests/run.sh in Ubuntu 24.04 with the same apt packages as .github/workflows/ci-linux.yml. The tree is bind-mounted read-only at /src.
Lint only, tests only, or custom command:
make test-linux-shellcheck
make test-linux-tests
bash scripts/test-linux-docker.sh bash scripts/shellcheck.sh
bash scripts/test-linux-docker.sh bash tests/run.shRun once on a real Mac with your usual shell:
quit --versionandquit --helpquit --dry-run Safari(or another installed app) — no processes quit- Quit a real test app you can afford to close
- A CLI tool you can restart (e.g.
quit --dry-runthen realquiton it) - Optional: ambiguous substring → picker → choose one
- Optional: bare
quitwith fzf — multi-select - Optional:
quit --confirm-sigkillon a disposable process and decline y at the SIGKILL prompt
This tool runs osascript, killall, and pgrep. It is aimed at local interactive use. Review targets (especially after substring resolution) before confirming SIGKILL.
| Path | Role |
|---|---|
quit |
Entry script (install on PATH as quit) |
VERSION |
Release version string for --version |
LICENSE |
MIT |
INSTALL.md |
Homebrew keg, source, Docker checks |
lib/log.sh |
Colors, info/warn/…, section, summary_bar |
lib/quit.sh |
Classification and escalation |
tests/run.sh |
Runs all tests/test-*.sh |
scripts/shellcheck.sh |
Local shellcheck driver |
.github/workflows/ci-macos.yml |
macOS CI (shellcheck + full tests) |
.github/workflows/ci-linux.yml |
Linux CI (shellcheck + tests; skips macOS-only file) |
docker/Dockerfile |
Linux test image (Ubuntu + shellcheck + psmisc / procps) |
docker-compose.yml |
docker compose run --rm test-linux — local Linux parity with CI |
scripts/test-linux-docker.sh |
Wrapper: runs Compose test-linux from repo root |
docker-compose.brew.yml |
homebrew-smoke — mount repo, run scripts/homebrew-smoke-inner.sh |
docker/Dockerfile.homebrew-smoke |
Optional baked image for the same smoke (no bind mount) |
scripts/homebrew-smoke-inner.sh |
Shared brew tap/install steps (Compose, Dockerfile, brew-smoke workflow) |
scripts/test-homebrew-docker.sh |
Wrapper: make brew-smoke — Docker smoke (no host install) |
.github/workflows/brew-smoke.yml |
workflow_dispatch + weekly: same smoke in Actions (not every push) |
Makefile |
make test-linux (and test-linux-shellcheck / test-linux-tests) |
CHANGELOG.md |
Release notes (Keep a Changelog) |
.github/workflows/release.yml |
On v* tag push: verify VERSION, create GitHub Release |
.github/dependabot.yml |
Weekly GitHub Actions dependency PRs |
-
Update
CHANGELOG.mdand bumpVERSION(Semantic Versioning). -
Run
bash scripts/shellcheck.shandbash tests/run.shon macOS (orbash scripts/test-linux-docker.sh/make test-linuxfor Linux parity). -
Run the manual smoke list above.
-
Commit, then tag and push (triggers the Release workflow and publishes notes):
git tag -a vX.Y.Z -m "Release vX.Y.Z" git push origin main && git push origin vX.Y.Z
-
Update the Homebrew formula in
mlevin2/homebrew-tap: seturlto the new tag archive andsha256(curl -sL …/archive/refs/tags/vX.Y.Z.tar.gz | shasum -a 256). Optionally run Actions → Brew smoke (workflow_dispatch) orbash scripts/test-homebrew-docker.shlocally. -
Ensure
LICENSEcopyright year / holder is correct for your legal needs (see note below).
Copyright: LICENSE lists Marshall Levin (2026). Adjust if your situation requires a different legal notice.