Skip to content

Instantly share code, notes, and snippets.

@matjam
Created March 11, 2026 07:35
Show Gist options
  • Select an option

  • Save matjam/85c3d73ed36a766fc7edcf7de233269e to your computer and use it in GitHub Desktop.

Select an option

Save matjam/85c3d73ed36a766fc7edcf7de233269e to your computer and use it in GitHub Desktop.

Critical Analysis: Kessler Codebase

Kessler is a ~3,700 line Go CLI tool that scans a developer's filesystem for build artifacts and deletes them — either to trash or permanently via os.RemoveAll. It includes an auto-pilot daemon that runs unattended via cron/launchd. The stakes for bugs in this codebase are high: a misidentified artifact means deleted source code.


I. CRITICAL — Data Loss Vectors

1. Git safety check fails open

engine/scanner.go:445-456isTrackedByGit() shells out to git ls-files. On any error (git not installed, corrupt index, permission denied, timeout under concurrent load), it returns false — "not tracked, safe to delete." This is the primary defense against deleting source code and it silently disables itself. It should fail closed.

2. Daemon auto-deletes with no confirmation

cmd/daemon.go:59-116runDaemonCheck() scans the entire $HOME, collects artifacts older than 10 days exceeding 1GB, and trashes them with zero user interaction, zero logging, and no exclusion support (line 61 passes nil for exclusions). Combined with finding #1, if git errors under load, the daemon could trash tracked source code at 2 AM.

3. filepath.Abs error discarded in daemon

cmd/daemon.go:97absPath, _ := filepath.Abs(a.Path). If this fails, absPath is "" and MoveToTrash("") is called. Behavior on an empty path is platform-dependent and unpredictable.

4. --force auto-escalates to permanent deletion

cmd/clean.go:249-260 — When --force is used and trash fails (e.g., cross-partition), the code silently escalates to os.RemoveAll() — irrecoverable permanent deletion — without re-asking the user. The user specified --force to skip the initial confirmation, not to authorize permanent destruction.

5. CleanGlobalCache fallback nukes entire directories

engine/global.go:302-303 — If the native cache clean command doesn't exist, the fallback is os.RemoveAll(cache.Path). For the Nix store (/nix/store), Cargo cache, or any path where the command isn't installed, this could destroy entire managed environments.

6. Crontab wipe on failed uninstall

cmd/daemon.go:325-330uninstallLinuxCron() discards the error from os.WriteFile for the temp crontab file. If the write fails, crontab tmpFile installs an empty/corrupt crontab, potentially wiping all of the user's cron jobs.


II. HIGH — Security Vulnerabilities

7. Supply chain attack via community rules

cmd/rules.go:25-56 — Downloads YAML from a hardcoded GitHub raw URL with no timeout, no size limit, no checksum, no signature, and no content validation. A compromised repo or MITM could serve rules that target src/, .git/, or any directory. Those rules then get auto-applied by the daemon.

8. DangerZone bypass via path variants and globs

engine/scanner.go:127-137 — Danger zone uses exact string equality (target.Path == dangerItem). A glob pattern like ".e*" matches .env via filepath.Glob but passes the danger zone check because ".e*" != ".env". Path variants (./.env, .env/) also bypass it.

9. Command injection in daemon scheduler installation

  • cmd/daemon.go:267-268exePath interpolated directly into a crontab line. A binary at a path containing ; rm -rf / # produces arbitrary cron execution.
  • cmd/daemon.go:180-210exePath and home interpolated into plist XML without escaping.
  • cmd/daemon.go:241-248exePath embedded in schtasks /tr argument with manual quoting.

10. PowerShell injection on Windows

engine/trash_windows.go:13-27 — File path embedded in PowerShell script. While single quotes are escaped, backtick characters (valid in NTFS filenames) are PowerShell escape characters and are not handled.

11. os.UserHomeDir error discarded in 9 locations

cmd/daemon.go:153,181,230 and engine/doctor.go:58,111,169,218,267,325 — When home is "", subsequent filepath.Join constructs paths rooted at /. On macOS, installMacLaunchAgent (line 181) would write a LaunchAgent plist to /Library/LaunchAgents/ — a system directory.


III. HIGH — Overly Broad Deletion Targets

12. Generic directory names in default rules

assets/default-rules.yaml — Several rules target directory names that are commonly used for non-artifact purposes:

Target Tier Risk
env (Python, line 69) safe Environment config directories
build (6 rules) safe/deep Dockerfiles, CI scripts, Makefiles
dist, out, bin safe/deep Scripts, compiled tools, data output
coverage (Node, line 44) safe Non-regeneratable reports
vendor (Go, line 112) safe Intentionally committed for reproducibility
*.log (LaTeX, line 301) safe Application/server logs in any dir with a .tex file

13. scanGitIgnored surfaces user data for deletion

engine/scanner.go:458-586 — Any directory in .gitignore not covered by existing rules is surfaced as TierIgnored. Common gitignored directories like data/, secrets/, backups/, uploads/, and database files are gitignored because they're sensitive, not because they're safe to delete.

14. R project detection triggers on DESCRIPTION file

assets/default-rules.yaml:274DESCRIPTION is an extremely generic filename. Any directory containing it is classified as an R project, and .RData files (potentially non-regeneratable analysis results) are targeted.


IV. CRITICAL — Developer Practices

15. Zero tests

There are 0 test files in the entire codebase. Zero. For a tool whose primary function is deleting files from the filesystem. The daemon auto-deletion logic, danger zone safety net, git tracking check, rule matching, config merging, glob handling — none of it has a single test. This is the single most important finding.

16. Nothing is testable even if tests existed

There are 0 interfaces in the codebase. Every function directly calls os.Stat, exec.Command, filepath.WalkDir, os.RemoveAll, os.ReadFile, etc. The Scanner struct cannot be tested without hitting the real filesystem and requiring real git, npm, cargo, etc. to be installed. No dependency injection exists.

17. No CI pipeline

.github/workflows/release.yml triggers only on tag push for GoReleaser. There is no CI workflow for PRs or pushes. No go test, go vet, staticcheck, or golangci-lint runs at any point. Code is compiled and shipped without any automated quality gate.


V. HIGH — Code Quality

18. 1833-line god file

internal/tui/tui.go is 1833 lines with View() spanning 871 lines (lines 891-1762) and Update() at 423 lines (lines 437-860). The entire TUI — model definition, all 4 tabs, all dialogs, scanning view, cleaning view, file tree renderer, sparkline renderer, icon mapping — is in one file.

19. ~300 lines of copy-pasted scan logic

engine/scanner.go:61-202 (Scan) and engine/scanner.go:214-380 (ScanWithProgress) share ~90% identical code. The walk logic, rule matching, danger zone checking, glob expansion, git safety, and project assembly are duplicated.

20. Deletion logic implemented 3 times

Three independent deletion loops exist: cmd/clean.go:218-239, internal/tui/tui.go:316-334, and cmd/daemon.go:96-101. Each makes different safety decisions. The daemon has no fallback logic. The TUI has automatic escalation to permanent deletion. The CLI has conditional escalation with --force.

21. No logging whatsoever

Not a single log.Printf, slog.Info, or any logging call exists. The daemon runs unattended with no way to diagnose failures. Failed deletions, failed git checks, failed cron operations — all invisible.

22. 40+ hardcoded magic numbers

Channel buffer sizes, progress intervals, pagination offsets, staleness thresholds, size thresholds, truncation lengths, modal dimensions — all inline constants with no names or documentation.

23. http.Get with no timeout

cmd/rules.go:27 — The community rules download has no context.Context, no http.Client with timeout, no deadline. On a hanging network, this blocks the process indefinitely.

24. No context.Context anywhere

No scan, clean, or external command call accepts a context. There is no way to cancel a long-running operation. The TUI scan can leak goroutines if the user quits during scanning.


VI. MEDIUM — Error Handling

25. 20+ silently swallowed errors

Across the codebase, errors from os.UserHomeDir, filepath.Abs, filepath.Match, os.MkdirAll, os.WriteFile, os.Remove, exec.Command().Run(), launchctl unload, crontab manipulation, beeep.Notify, lsof, and PID parsing are all discarded with _. The history persistence (engine/history.go:61-62) discards both MkdirAll and WriteFile errors.

26. Non-atomic file writes

engine/history.go:62 uses os.WriteFile which truncates then writes. A crash mid-write corrupts history. cmd/rules.go:47-56 creates then writes the community rules file; a network failure after os.Create leaves an empty file, destroying previously downloaded rules.

27. No config validation

engine/types.goTier is a string alias with no validation. tier: "yolo" in YAML is silently accepted. A malformed ~/.config/kessler/rules.yaml makes the entire tool unusable with no fallback-to-defaults option.


VII. MEDIUM — Concurrency Issues

28. Goroutine leak if TUI exits during scan

engine/scanner.go:287-368 and internal/tui/tui.go:221-242 — Worker goroutines send on the progress channel (buffer 10). If the TUI consumer stops reading (user quits), goroutines block forever on channel send, wg.Wait() never returns, and the outer goroutine is leaked.

29. History file race condition (multi-process)

engine/history.go:46-63SaveEntry does a read-modify-write cycle on history.json with no file locking. The daemon and an interactive session running simultaneously can clobber each other's writes.

30. Non-atomic history write creates corruption window

engine/history.go:62os.WriteFile truncates then writes. If the process is killed mid-write (OOM, power loss during daemon), the file is left truncated. Next LoadHistory finds corrupt JSON and silently returns empty history — all history is lost.


VIII. LOW-MEDIUM — Additional Issues

31. formatBytes() duplicated between packages

cmd/scan.go:182-193 and internal/tui/tui.go:151-162 — Identical function defined twice.

32. doctor.go has 5 near-identical functions

getUnusedNode, getUnusedRust, getUnusedPython, getUnusedRuby, getUnusedJava all follow the exact same pattern (~50 lines each) with only file names and commands changing. Should be table-driven.

33. Rust toolchain version detection reads entire TOML as version string

engine/doctor.go:348-352 — The entire contents of rust-toolchain.toml is treated as a version string instead of parsing the TOML. The in-use detection will never match installed toolchains, causing all Rust toolchains to be reported as "unused."

34. "OS & Editor Caches" rule has empty triggers but code skips empty triggers

assets/default-rules.yaml:422-438 — The rule has triggers: [] with a comment saying "applies everywhere globally," but scanner.go:382 skips rules with empty triggers. The rule is inert — a mismatch between intent and implementation.

35. GoReleaser ldflags reference undefined variables

.goreleaser.yaml:24 sets -X main.version={{.Version}} -X main.commit={{.ShortCommit}} but main.go has no version or commit variables defined. These linker flags silently do nothing.

36. Package-level mutable globals

cmd/root.go:14-23RulesData, CommunityRulesData, UserRulesData are mutable exported package-level variables set from main.go. Should be passed as constructor arguments.

37. Presentation concerns leaked into engine types

engine/global.goGlobalCache.Icon (an emoji) and GlobalCache.CleanCommand (display text) are presentation-layer concerns embedded in engine data types.

38. Exclusion pattern matching uses substring

engine/scanner.go:95strings.Contains(path, pattern) is a substring match on the full path. An exclusion pattern of "o" would skip any directory whose path contains the letter "o."

39. Single-keypress permanent deletion in TUI

internal/tui/tui.go:482-509X then y permanently deletes potentially gigabytes of data. a then X then y = select ALL then permanently delete ALL. No "type YES to confirm" safeguard for destructive operations.

40. No Makefile or build script

There is no Makefile, Taskfile, or justfile. Developers have no standardized way to build, test, lint, or run the project.


IX. Summary

Severity Count Theme
CRITICAL 8 Git safety fails open; daemon auto-deletes untested; zero tests; untestable architecture; no CI
HIGH 10 Supply chain attack; command injection; overly broad targets; god file; code duplication; no logging
MEDIUM 9 Swallowed errors; non-atomic writes; no config validation; no context; magic numbers; TOCTOU
LOW 13 Duplicated helpers; broken version detection; inert rules; mutable globals

X. If I Could Fix Only 5 Things

  1. Flip isTrackedByGit to fail closed — if git errors, assume tracked, refuse to delete. This is a one-line change that eliminates the largest data loss vector.

  2. Make the daemon notify-only — scan and report what would be cleaned, but require the user to run kessler clean to act. Auto-deletion without tests is reckless.

  3. Add filesystem and command interfaces — make the engine testable, then write tests for danger zone checking, rule matching, and config merging.

  4. Add a CI pipelinego vet, staticcheck, and go test on every PR.

  5. Validate community rules — parse the YAML, reject rules targeting paths shorter than 3 characters or matching known source directories, add a size limit and timeout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment