From aef686af8611fd7c890365a27f850185c7a31168 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 21 Sep 2026 21:26:40 -0400 Subject: [PATCH 1/7] feat(docs): add CLASSIFICATION function-header field, rename history shadow Rename the C1 history() shadow to pretty-history so it never collides with the fish builtin -- every function expecting stock history semantics (search, --max, merge, ...) would otherwise silently break. hist.fish, which relied on the shadow's timestamp formatting, now requests it explicitly via builtin history --show-time. Add a CLASSIFICATION doc-header label so a function can declare its interaction with C1-shadowed commands (uses-shadow/bypasses-shadow) and general hazards (destructive, network, blocking-prompt) for anyone deciding to disable an opinionated category or call the function from automation. Wired into the manual/site build pipeline (manualtools.py, build-manual.py) and the C1 shadow doc gets a new "For function authors" bypass-mechanism reference table (command/builtin/__original_help, and which shadows have no real bypass target at all). --- conf.d/tricks.fish | 10 ++-- docs/build-manual.py | 25 ++++++++++ .../01-c1-command-shadows.md | 50 ++++++++++++++++++- docs/manualtools.py | 1 + functions/hist.fish | 2 +- 5 files changed, 82 insertions(+), 6 deletions(-) diff --git a/conf.d/tricks.fish b/conf.d/tricks.fish index 6c82fb1..43f8705 100644 --- a/conf.d/tricks.fish +++ b/conf.d/tricks.fish @@ -97,11 +97,13 @@ if __fish_config_op_enabled (status basename) tricks-bang end end -# Fish command history override to show timestamps -# Shadowing the history command is opinionated (C1 aliasing); when disabled, -# the function is never defined and fish's stock history behavior applies. +# Timestamped history view. Named pretty-history (not history) so it never +# shadows the fish builtin -- every function in this config that expects +# stock `history` semantics (search, --max, merge, ...) would otherwise +# silently break, which has happened more than once. Opinionated (C1 +# aliasing); when disabled, the function is never defined. if __fish_config_op_enabled (status basename) aliases-tricks - function history + function pretty-history --description 'History with timestamps prepended to every entry' builtin history --show-time='%F %T ' end end diff --git a/docs/build-manual.py b/docs/build-manual.py index e80ebca..cb7ac5f 100644 --- a/docs/build-manual.py +++ b/docs/build-manual.py @@ -703,6 +703,29 @@ ENTRY_HEADS = { } +def _classification_tags(raw: list[str]) -> list[str]: + """Split a CLASSIFICATION body into its comma-separated tags. + + A plain comma split (as `names()` uses for DEPENDENCIES) would break on + the commas inside `uses-shadow(rm, cp)`-style tags, so this only splits + on commas at paren depth 0. + """ + text = " ".join(raw) + tags: list[str] = [] + depth = 0 + start = 0 + for i, ch in enumerate(text): + if ch == "(": + depth += 1 + elif ch == ")": + depth = max(0, depth - 1) + elif ch == "," and depth == 0: + tags.append(text[start:i].strip()) + start = i + 1 + tags.append(text[start:].strip()) + return [t for t in tags if t] + + def render_entry(fn: dict[str, list[str]], used_by: list[str], link=None) -> str: """Render one parsed function header as a manual entry body. @@ -739,6 +762,7 @@ def render_entry(fn: dict[str, list[str]], used_by: list[str], link=None) -> str refs = [] for label, values in ( ("Dependencies", names(fn.get("DEPENDENCIES", []))), + ("Classification", _classification_tags(fn.get("CLASSIFICATION", []))), ("Used by", sorted(used_by)), ): if values: @@ -884,6 +908,7 @@ def render_entry_site(fn: dict[str, list[str]], used_by: list[str], link=None) - refs = [] for label, values in ( ("Dependencies", names(fn.get("DEPENDENCIES", []))), + ("Classification", _classification_tags(fn.get("CLASSIFICATION", []))), ("Used by", sorted(used_by)), ): if values: diff --git a/docs/manual/08-components-reference/01-c1-command-shadows.md b/docs/manual/08-components-reference/01-c1-command-shadows.md index eb3b5fb..a71900c 100644 --- a/docs/manual/08-components-reference/01-c1-command-shadows.md +++ b/docs/manual/08-components-reference/01-c1-command-shadows.md @@ -19,7 +19,6 @@ all of these commands. rg rg --hyperlink-format=kitty system rg mkdir verbose path-tree display on creation mkdir -p silently bash XDG bashrc + $SHELL reset on exit system bash - history timestamps prepended to every entry fish builtin history cp / mv forced -i confirmation prompt cp / mv unmodified wget forced --continue (resume downloads) system wget grep/fgrep/egrep forced --color=auto system grep variants @@ -31,6 +30,11 @@ all of these commands. When C1 is disabled, `rm` uses bare `command rm` with no wrapper — files are permanently deleted, not trashed. There is no intermediate safety net. +`history` itself is never shadowed — every function in this config that +reads history depends on its stock builtin semantics. `pretty-history` +(same `aliases-tricks` toggle) is a separate command that prints history +with a timestamp prepended to every entry. + ## Sub-categories `__fish_config_op_aliases` sub-divides into six sub-categories, each with @@ -63,3 +67,47 @@ and the `help config` interception. `claude` (AGENTS.md/CLAUDE.md auto-linking) and `edit` (multi-editor launcher), plus `agy`. +## For function authors + +Calling one of these names bare from inside your own function means the +override runs whenever C1 (or its sub-category) is on — which may not be +what your function wants: a shadow can change stdout (`cat`'s syntax +highlighting, `mkdir`'s tree display), prompt interactively where none is +expected (`cp`/`mv`'s forced `-i`), or reshape output structurally (`ls`'s +icons/columns, `rg`'s hyperlink markers). If your function's logic depends +on stock behavior, bypass the shadow deterministically, regardless of the +toggle state: + + Shadow Bypass Why + ───────────────────────────────────────────────────────────────────────── + ls, cat, rm, less, du, command Real external + top, ping, ssh, rg, binaries — a + mkdir, bash, cp, mv, real system command + wget, grep/fgrep/egrep, exists to fall + dir/vdir, claude back to. + cd builtin cd The one true + fish builtin + in this table. + help config __original_help $argv `help` is neither + a builtin nor an + external binary + (embedded in the + fish binary + itself) — see + conf.d/help.fish + for why the + wrapper keeps its + own backup copy. + edit (nothing to bypass to) Purely our own + invention, no + stock command + exists. Call + $EDITOR/$VISUAL + yourself if you + want a plain + editor launch. + +A function's own doc header records which of these it depends on: see the +`CLASSIFICATION` label (`uses-shadow(...)` / `bypasses-shadow(...)`) in +`AGENTS/functions/CLAUDE.md`. + diff --git a/docs/manualtools.py b/docs/manualtools.py index 70054a2..5a66d92 100644 --- a/docs/manualtools.py +++ b/docs/manualtools.py @@ -64,6 +64,7 @@ SECTIONS = ( "CATEGORY", "COMPONENT", "DEPENDENCIES", + "CLASSIFICATION", "SYNOPSIS", "DESCRIPTION", "ARGUMENTS", diff --git a/functions/hist.fish b/functions/hist.fish index c0b7c7a..51c0f0d 100644 --- a/functions/hist.fish +++ b/functions/hist.fish @@ -34,7 +34,7 @@ function hist --description 'Search fish history and put it in the prompt' return 1 end - set -l selected (history | fzf --reverse --height 40% --with-nth 3..) + set -l selected (builtin history --show-time='%F %T ' | fzf --reverse --height 40% --with-nth 3..) if test -n "$selected" # Strip the timestamp for the final output -- 2.54.0 From 859f14a6e90e18430cf98f3c4eddd84f43049a20 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 21 Sep 2026 21:26:44 -0400 Subject: [PATCH 2/7] feat(functions): tag CLASSIFICATION across functions/ and conf.d/ Audits every function's interaction with the C1-shadowed commands (uses-shadow/bypasses-shadow) and general hazards (destructive, network, blocking-prompt) per the CLASSIFICATION schema. Delegated the initial mechanical sweep to agy, then reviewed every file by hand: fixed a systemic double-blank-comment-line formatting bug from the delegate pass, and corrected several judgment errors found on review -- three false blocking-prompt tags where a fish 'read' was consuming piped input rather than waiting on a terminal (open-url.fish, sbver.fish, play-media.fish, now untagged entirely), a blocking-prompt tag on mkrep.fish despite its documented --yes escape hatch, an untagged read in jobrunner.fish's own baseless blocking-prompt claim (removed, along with a destructive tag on cleanup of its own mktemp output -- the schema explicitly excludes that), the same own-output-cleanup false positive on _zellij_dump_log.fish's destructive tag, an interactive fzf-gated confirmation on logs.fish and replay.fish's piped read misread the same way as the first three, and a uses-shadow(mkdir) on mkcd.fish that actually belongs to the _fish_mkdir_p helper it delegates to, not to mkcd itself. --- conf.d/auto-pull.fish | 3 +++ functions/__fish_help_header.fish | 3 +++ functions/__fish_user_dots_link.fish | 3 +++ functions/__kitty_logging_has_watcher.fish | 3 +++ functions/_agents_repo_install_tools.fish | 3 +++ functions/_fish_deps_install.fish | 3 +++ functions/_fish_deps_marktext_appimage.fish | 3 +++ functions/_fish_deps_update.fish | 3 +++ functions/_fish_mkdir_p.fish | 3 +++ functions/_mkrep_default_remote_cmd.fish | 3 +++ functions/_mkrep_repo_exists.fish | 3 +++ functions/_prune_terminal_logs.fish | 3 +++ functions/_scrollback_prune_junk.fish | 3 +++ functions/_tmux_pipe_log.fish | 3 +++ functions/_zellij_dump_log.fish | 3 +++ functions/bash.fish | 3 +++ functions/cat.fish | 3 +++ functions/claude-docs.fish | 3 +++ functions/claude-pr.fish | 3 +++ functions/claude.fish | 3 +++ functions/copy.fish | 3 +++ functions/dockup.fish | 3 +++ functions/du.fish | 3 +++ functions/dusize.fish | 3 +++ functions/fzf-update.fish | 3 +++ functions/gi.fish | 3 +++ functions/gip.fish | 3 +++ functions/gip4.fish | 3 +++ functions/gip6.fish | 3 +++ functions/git-clean.fish | 3 +++ functions/gitup.fish | 3 +++ functions/jobrunner.fish | 3 +++ functions/kitty-logging.fish | 3 +++ functions/lD.fish | 3 +++ functions/less.fish | 3 +++ functions/logs.fish | 3 +++ functions/ls.fish | 3 +++ functions/lsr.fish | 3 +++ functions/lss.fish | 3 +++ functions/lstree.fish | 3 +++ functions/lt.fish | 3 +++ functions/ltr.fish | 3 +++ functions/lx.fish | 3 +++ functions/md.fish | 3 +++ functions/mkcd.fish | 3 +++ functions/mkdir.fish | 3 +++ functions/mkrep.fish | 3 +++ functions/mv.fish | 3 +++ functions/parur.fish | 3 +++ functions/ping.fish | 3 +++ functions/pkg.fish | 3 +++ functions/qr.fish | 3 +++ functions/rand_string.fish | 3 +++ functions/replay.fish | 3 +++ functions/rg.fish | 3 +++ functions/rm.fish | 3 +++ functions/scrub.fish | 3 +++ functions/search.fish | 3 +++ functions/smart_exit.fish | 3 +++ functions/ssh.fish | 3 +++ functions/superpowers.fish | 3 +++ functions/top.fish | 3 +++ functions/upgrade.fish | 3 +++ functions/view.fish | 3 +++ functions/yt-dlp.fish | 3 +++ 65 files changed, 195 insertions(+) diff --git a/conf.d/auto-pull.fish b/conf.d/auto-pull.fish index db4f7f5..67b354a 100644 --- a/conf.d/auto-pull.fish +++ b/conf.d/auto-pull.fish @@ -24,6 +24,9 @@ __fish_config_op_enabled (status basename); or exit # COMPONENT # autoexec/sync # +# CLASSIFICATION +# bypasses-shadow(cat) +# # SYNOPSIS # __auto_pull_on_pwd (event handler, --on-variable PWD) # diff --git a/functions/__fish_help_header.fish b/functions/__fish_help_header.fish index 313ebad..c34f61b 100644 --- a/functions/__fish_help_header.fish +++ b/functions/__fish_help_header.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# bypasses-shadow(cat) +# # SYNOPSIS # __fish_help_header [args...] # diff --git a/functions/__fish_user_dots_link.fish b/functions/__fish_user_dots_link.fish index 1a5d46b..d7026b8 100644 --- a/functions/__fish_user_dots_link.fish +++ b/functions/__fish_user_dots_link.fish @@ -4,6 +4,9 @@ # COMPONENT # autoexec/sync # +# CLASSIFICATION +# destructive +# # SYNOPSIS # __fish_user_dots_link # diff --git a/functions/__kitty_logging_has_watcher.fish b/functions/__kitty_logging_has_watcher.fish index 3455eee..2cee31a 100644 --- a/functions/__kitty_logging_has_watcher.fish +++ b/functions/__kitty_logging_has_watcher.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# bypasses-shadow(grep) +# # SYNOPSIS # __kitty_logging_has_watcher # diff --git a/functions/_agents_repo_install_tools.fish b/functions/_agents_repo_install_tools.fish index 95aeb83..b890899 100644 --- a/functions/_agents_repo_install_tools.fish +++ b/functions/_agents_repo_install_tools.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# uses-shadow(mkdir), bypasses-shadow(cp,grep) +# # SYNOPSIS # _agents_repo_install_tools # diff --git a/functions/_fish_deps_install.fish b/functions/_fish_deps_install.fish index bcb72bf..e1a1f53 100644 --- a/functions/_fish_deps_install.fish +++ b/functions/_fish_deps_install.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# destructive, network, blocking-prompt +# # SYNOPSIS # _fish_deps_install # diff --git a/functions/_fish_deps_marktext_appimage.fish b/functions/_fish_deps_marktext_appimage.fish index 123f005..a84c09e 100644 --- a/functions/_fish_deps_marktext_appimage.fish +++ b/functions/_fish_deps_marktext_appimage.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# destructive, network +# # SYNOPSIS # _fish_deps_marktext_appimage # diff --git a/functions/_fish_deps_update.fish b/functions/_fish_deps_update.fish index a78e688..2368d36 100644 --- a/functions/_fish_deps_update.fish +++ b/functions/_fish_deps_update.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# destructive, network +# # SYNOPSIS # _fish_deps_update # diff --git a/functions/_fish_mkdir_p.fish b/functions/_fish_mkdir_p.fish index 29f25b2..622b64d 100644 --- a/functions/_fish_mkdir_p.fish +++ b/functions/_fish_mkdir_p.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# uses-shadow(mkdir) +# # SYNOPSIS # _fish_mkdir_p [--path|--tree|--silent] # diff --git a/functions/_mkrep_default_remote_cmd.fish b/functions/_mkrep_default_remote_cmd.fish index a39bcdf..01bf424 100644 --- a/functions/_mkrep_default_remote_cmd.fish +++ b/functions/_mkrep_default_remote_cmd.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# network +# # SYNOPSIS # _mkrep_default_remote_cmd # diff --git a/functions/_mkrep_repo_exists.fish b/functions/_mkrep_repo_exists.fish index e70b841..74e7492 100644 --- a/functions/_mkrep_repo_exists.fish +++ b/functions/_mkrep_repo_exists.fish @@ -4,6 +4,9 @@ # DEPENDENCIES # gh, glab, tea # +# CLASSIFICATION +# network +# # SYNOPSIS # _mkrep_repo_exists # diff --git a/functions/_prune_terminal_logs.fish b/functions/_prune_terminal_logs.fish index 5a8b546..a764e59 100644 --- a/functions/_prune_terminal_logs.fish +++ b/functions/_prune_terminal_logs.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# bypasses-shadow(ls,rm), destructive +# # SYNOPSIS # _prune_terminal_logs # diff --git a/functions/_scrollback_prune_junk.fish b/functions/_scrollback_prune_junk.fish index 37f474d..4d8b165 100644 --- a/functions/_scrollback_prune_junk.fish +++ b/functions/_scrollback_prune_junk.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# bypasses-shadow(cat) +# # SYNOPSIS # _scrollback_prune_junk [dir] # diff --git a/functions/_tmux_pipe_log.fish b/functions/_tmux_pipe_log.fish index e4ed18a..ae9d98c 100644 --- a/functions/_tmux_pipe_log.fish +++ b/functions/_tmux_pipe_log.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# uses-shadow(mkdir) +# # SYNOPSIS # _tmux_pipe_log # diff --git a/functions/_zellij_dump_log.fish b/functions/_zellij_dump_log.fish index 0808b5d..6dd34d1 100644 --- a/functions/_zellij_dump_log.fish +++ b/functions/_zellij_dump_log.fish @@ -4,6 +4,9 @@ # COMPONENT # logging/multiplexer-capture # +# CLASSIFICATION +# uses-shadow(mkdir), bypasses-shadow(rm) +# # SYNOPSIS # _zellij_dump_log # diff --git a/functions/bash.fish b/functions/bash.fish index bfb721e..b6474e2 100644 --- a/functions/bash.fish +++ b/functions/bash.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/shell-tools # +# CLASSIFICATION +# bypasses-shadow(bash) +# # SYNOPSIS # bash [args...] # diff --git a/functions/cat.fish b/functions/cat.fish index e6394b8..ce494e7 100644 --- a/functions/cat.fish +++ b/functions/cat.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/filesystem # +# CLASSIFICATION +# uses-shadow(ls), bypasses-shadow(cat) +# # SYNOPSIS # cat [args...] # diff --git a/functions/claude-docs.fish b/functions/claude-docs.fish index 5ac679b..0f31b12 100644 --- a/functions/claude-docs.fish +++ b/functions/claude-docs.fish @@ -4,6 +4,9 @@ # CATEGORY # 12-ai-and-developer-tools # +# CLASSIFICATION +# uses-shadow(claude) +# # SYNOPSIS # claude-docs # diff --git a/functions/claude-pr.fish b/functions/claude-pr.fish index 1079251..f28eb41 100644 --- a/functions/claude-pr.fish +++ b/functions/claude-pr.fish @@ -4,6 +4,9 @@ # CATEGORY # 12-ai-and-developer-tools # +# CLASSIFICATION +# uses-shadow(claude) +# # SYNOPSIS # claude-pr # diff --git a/functions/claude.fish b/functions/claude.fish index 37285b5..ca245b0 100644 --- a/functions/claude.fish +++ b/functions/claude.fish @@ -10,6 +10,9 @@ # DEPENDENCIES # agents-init, agents-vault # +# CLASSIFICATION +# bypasses-shadow(claude) +# # SYNOPSIS # claude [ARGS...] # diff --git a/functions/copy.fish b/functions/copy.fish index e3ceb0e..2a5fbf6 100644 --- a/functions/copy.fish +++ b/functions/copy.fish @@ -4,6 +4,9 @@ # CATEGORY # 01-file-and-directory # +# CLASSIFICATION +# bypasses-shadow(cp) +# # SYNOPSIS # copy # diff --git a/functions/dockup.fish b/functions/dockup.fish index fa5b5fb..1717085 100644 --- a/functions/dockup.fish +++ b/functions/dockup.fish @@ -4,6 +4,9 @@ # CATEGORY # 14-miscellaneous # +# CLASSIFICATION +# network +# # SYNOPSIS # dockup [-h] [directory] # diff --git a/functions/du.fish b/functions/du.fish index ad03503..bb8924e 100644 --- a/functions/du.fish +++ b/functions/du.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/filesystem # +# CLASSIFICATION +# bypasses-shadow(du) +# # SYNOPSIS # du [--disk|--dir|--dua] [args...] # diff --git a/functions/dusize.fish b/functions/dusize.fish index d188c2a..cf05393 100644 --- a/functions/dusize.fish +++ b/functions/dusize.fish @@ -4,6 +4,9 @@ # CATEGORY # 01-file-and-directory # +# CLASSIFICATION +# uses-shadow(du) +# # SYNOPSIS # dusize [dir] # diff --git a/functions/fzf-update.fish b/functions/fzf-update.fish index b0c5ae9..543d99d 100644 --- a/functions/fzf-update.fish +++ b/functions/fzf-update.fish @@ -4,6 +4,9 @@ # CATEGORY # 06-dependency-management # +# CLASSIFICATION +# network +# # SYNOPSIS # fzf-update # diff --git a/functions/gi.fish b/functions/gi.fish index 2010a80..2ad3aef 100644 --- a/functions/gi.fish +++ b/functions/gi.fish @@ -4,6 +4,9 @@ # CATEGORY # 04-git-and-version-control # +# CLASSIFICATION +# network, blocking-prompt +# # SYNOPSIS # gi [-h] [-b] [-p] [-s] [-l] [targets...] # diff --git a/functions/gip.fish b/functions/gip.fish index bbbd9dd..83825b6 100644 --- a/functions/gip.fish +++ b/functions/gip.fish @@ -4,6 +4,9 @@ # CATEGORY # 10-network # +# CLASSIFICATION +# network +# # SYNOPSIS # gip # diff --git a/functions/gip4.fish b/functions/gip4.fish index a8f54b4..8d957cf 100644 --- a/functions/gip4.fish +++ b/functions/gip4.fish @@ -4,6 +4,9 @@ # CATEGORY # 10-network # +# CLASSIFICATION +# network +# # SYNOPSIS # gip4 # diff --git a/functions/gip6.fish b/functions/gip6.fish index d61b778..f272fb8 100644 --- a/functions/gip6.fish +++ b/functions/gip6.fish @@ -4,6 +4,9 @@ # CATEGORY # 10-network # +# CLASSIFICATION +# network +# # SYNOPSIS # gip6 # diff --git a/functions/git-clean.fish b/functions/git-clean.fish index b209429..64e1b31 100644 --- a/functions/git-clean.fish +++ b/functions/git-clean.fish @@ -4,6 +4,9 @@ # CATEGORY # 04-git-and-version-control # +# CLASSIFICATION +# network +# # SYNOPSIS # git-clean [-h] [-f] # diff --git a/functions/gitup.fish b/functions/gitup.fish index dea143e..b453de2 100644 --- a/functions/gitup.fish +++ b/functions/gitup.fish @@ -4,6 +4,9 @@ # CATEGORY # 04-git-and-version-control # +# CLASSIFICATION +# network +# # SYNOPSIS # gitup [args...] # diff --git a/functions/jobrunner.fish b/functions/jobrunner.fish index 9c5a24c..9ea5c54 100644 --- a/functions/jobrunner.fish +++ b/functions/jobrunner.fish @@ -7,6 +7,9 @@ # DEPENDENCIES # tmux, screen, __jobrunner_sessions # +# CLASSIFICATION +# bypasses-shadow(cat,rm) +# # SYNOPSIS # jobrunner [-t ] [] [] [...] # jr [-t ] [] [] [...] diff --git a/functions/kitty-logging.fish b/functions/kitty-logging.fish index 82eb2bc..fc2921c 100644 --- a/functions/kitty-logging.fish +++ b/functions/kitty-logging.fish @@ -7,6 +7,9 @@ # COMPONENT # logging/terminal-capture # +# CLASSIFICATION +# bypasses-shadow(grep,mkdir,rm) +# # SYNOPSIS # kitty-logging [install | uninstall | status | dismiss] [-h] # diff --git a/functions/lD.fish b/functions/lD.fish index 2a78257..2126153 100644 --- a/functions/lD.fish +++ b/functions/lD.fish @@ -4,6 +4,9 @@ # CATEGORY # 01-file-and-directory # +# CLASSIFICATION +# bypasses-shadow(ls) +# # SYNOPSIS # lD [args...] # diff --git a/functions/less.fish b/functions/less.fish index 11c034e..3680857 100644 --- a/functions/less.fish +++ b/functions/less.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/shell-tools # +# CLASSIFICATION +# bypasses-shadow(cat,less) +# # SYNOPSIS # less [args...] # diff --git a/functions/logs.fish b/functions/logs.fish index 91507e8..cee61b3 100644 --- a/functions/logs.fish +++ b/functions/logs.fish @@ -7,6 +7,9 @@ # COMPONENT # integrations/history-logs # +# CLASSIFICATION +# bypasses-shadow(cat), network +# # SYNOPSIS # logs [-h] [-c ] # diff --git a/functions/ls.fish b/functions/ls.fish index da11956..28f5ad3 100644 --- a/functions/ls.fish +++ b/functions/ls.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/filesystem # +# CLASSIFICATION +# bypasses-shadow(ls) +# # SYNOPSIS # ls [args...] # diff --git a/functions/lsr.fish b/functions/lsr.fish index a51bbe8..593b43d 100644 --- a/functions/lsr.fish +++ b/functions/lsr.fish @@ -4,6 +4,9 @@ # CATEGORY # 01-file-and-directory # +# CLASSIFICATION +# bypasses-shadow(ls) +# # SYNOPSIS # lsr [args...] # diff --git a/functions/lss.fish b/functions/lss.fish index 7118254..b50e60c 100644 --- a/functions/lss.fish +++ b/functions/lss.fish @@ -4,6 +4,9 @@ # CATEGORY # 01-file-and-directory # +# CLASSIFICATION +# bypasses-shadow(ls) +# # SYNOPSIS # lss [args...] # diff --git a/functions/lstree.fish b/functions/lstree.fish index f74da5d..bc781c1 100644 --- a/functions/lstree.fish +++ b/functions/lstree.fish @@ -4,6 +4,9 @@ # CATEGORY # 01-file-and-directory # +# CLASSIFICATION +# bypasses-shadow(ls) +# # SYNOPSIS # lstree [args...] # diff --git a/functions/lt.fish b/functions/lt.fish index 29ca192..366708e 100644 --- a/functions/lt.fish +++ b/functions/lt.fish @@ -4,6 +4,9 @@ # CATEGORY # 01-file-and-directory # +# CLASSIFICATION +# bypasses-shadow(ls) +# # SYNOPSIS # lt [args...] # diff --git a/functions/ltr.fish b/functions/ltr.fish index c9d34e0..2a077e6 100644 --- a/functions/ltr.fish +++ b/functions/ltr.fish @@ -4,6 +4,9 @@ # CATEGORY # 01-file-and-directory # +# CLASSIFICATION +# bypasses-shadow(ls) +# # SYNOPSIS # ltr [args...] # diff --git a/functions/lx.fish b/functions/lx.fish index ea10d4d..0a320f7 100644 --- a/functions/lx.fish +++ b/functions/lx.fish @@ -4,6 +4,9 @@ # CATEGORY # 01-file-and-directory # +# CLASSIFICATION +# bypasses-shadow(ls) +# # SYNOPSIS # lx [args...] # diff --git a/functions/md.fish b/functions/md.fish index 2a639f7..b7332d3 100644 --- a/functions/md.fish +++ b/functions/md.fish @@ -7,6 +7,9 @@ # DEPENDENCIES # marktext, firejail, bkg # +# CLASSIFICATION +# uses-shadow(mkdir) +# # SYNOPSIS # md [-r] [--foreground] [marktext-args...] [FILE...] # diff --git a/functions/mkcd.fish b/functions/mkcd.fish index 5cf8c30..161a77a 100644 --- a/functions/mkcd.fish +++ b/functions/mkcd.fish @@ -4,6 +4,9 @@ # CATEGORY # 01-file-and-directory # +# CLASSIFICATION +# uses-shadow(cd) +# # SYNOPSIS # mkcd [-s | --silent] # diff --git a/functions/mkdir.fish b/functions/mkdir.fish index cf013d6..d33f535 100644 --- a/functions/mkdir.fish +++ b/functions/mkdir.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/filesystem # +# CLASSIFICATION +# bypasses-shadow(mkdir) +# # SYNOPSIS # mkdir [args...] # diff --git a/functions/mkrep.fish b/functions/mkrep.fish index f57e5e1..725ba33 100644 --- a/functions/mkrep.fish +++ b/functions/mkrep.fish @@ -9,6 +9,9 @@ # _mkrep_add_origin, _mkrep_default_remote_cmd, _mkrep_remote_url, # _mkrep_repo_exists, git # +# CLASSIFICATION +# uses-shadow(cd), destructive, network +# # SYNOPSIS # mkrep [--cd | --no-cd] [--mkdir | --no-mkdir] [--git | --no-git] # [-c | --clean | --no-clean] [--strict] [-v | --verbose] diff --git a/functions/mv.fish b/functions/mv.fish index 82efcf1..3f9aa6f 100644 --- a/functions/mv.fish +++ b/functions/mv.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/filesystem # +# CLASSIFICATION +# bypasses-shadow(mv) +# # SYNOPSIS # mv [args...] # diff --git a/functions/parur.fish b/functions/parur.fish index 6f6f92c..79f182b 100644 --- a/functions/parur.fish +++ b/functions/parur.fish @@ -4,6 +4,9 @@ # CATEGORY # 05-package-management # +# CLASSIFICATION +# network +# # SYNOPSIS # parur # diff --git a/functions/ping.fish b/functions/ping.fish index 947d597..244608d 100644 --- a/functions/ping.fish +++ b/functions/ping.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/network # +# CLASSIFICATION +# bypasses-shadow(ping) +# # SYNOPSIS # ping [args...] # diff --git a/functions/pkg.fish b/functions/pkg.fish index 2090b50..0f385f7 100644 --- a/functions/pkg.fish +++ b/functions/pkg.fish @@ -4,6 +4,9 @@ # CATEGORY # 05-package-management # +# CLASSIFICATION +# network +# # SYNOPSIS # pkg [-h] [-i|-u] [package...] # diff --git a/functions/qr.fish b/functions/qr.fish index 268c7be..e001f86 100644 --- a/functions/qr.fish +++ b/functions/qr.fish @@ -4,6 +4,9 @@ # CATEGORY # 10-network # +# CLASSIFICATION +# network +# # SYNOPSIS # qr [text...] # diff --git a/functions/rand_string.fish b/functions/rand_string.fish index 2d96f5d..0a9b7d4 100644 --- a/functions/rand_string.fish +++ b/functions/rand_string.fish @@ -4,6 +4,9 @@ # CATEGORY # 14-miscellaneous # +# CLASSIFICATION +# bypasses-shadow(cat) +# # SYNOPSIS # rand_string [COMPONENTS/MODIFIERS]... # diff --git a/functions/replay.fish b/functions/replay.fish index 7a7ce22..3b8f3af 100644 --- a/functions/replay.fish +++ b/functions/replay.fish @@ -4,6 +4,9 @@ # CATEGORY # 14-miscellaneous # +# CLASSIFICATION +# bypasses-shadow(bash,cd) +# # SYNOPSIS # replay # diff --git a/functions/rg.fish b/functions/rg.fish index 0c164cb..44173d3 100644 --- a/functions/rg.fish +++ b/functions/rg.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/search # +# CLASSIFICATION +# bypasses-shadow(rg) +# # SYNOPSIS # rg [args...] # diff --git a/functions/rm.fish b/functions/rm.fish index bbcd46c..50c452a 100644 --- a/functions/rm.fish +++ b/functions/rm.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/filesystem # +# CLASSIFICATION +# bypasses-shadow(rm), destructive +# # SYNOPSIS # rm [-e [options] | -S | args...] # diff --git a/functions/scrub.fish b/functions/scrub.fish index 6686a6c..7444c81 100644 --- a/functions/scrub.fish +++ b/functions/scrub.fish @@ -4,6 +4,9 @@ # CATEGORY # 01-file-and-directory # +# CLASSIFICATION +# bypasses-shadow(rm), destructive +# # SYNOPSIS # scrub [-a] [-d] [-h] # diff --git a/functions/search.fish b/functions/search.fish index e6a69c1..bd2560b 100644 --- a/functions/search.fish +++ b/functions/search.fish @@ -4,6 +4,9 @@ # CATEGORY # 05-package-management # +# CLASSIFICATION +# network +# # SYNOPSIS # search [args...] # diff --git a/functions/smart_exit.fish b/functions/smart_exit.fish index bbb73dc..0b4babb 100644 --- a/functions/smart_exit.fish +++ b/functions/smart_exit.fish @@ -8,6 +8,9 @@ # site exit-plain: overrides/key-bindings # site logging-guard: logging/terminal-capture # +# CLASSIFICATION +# destructive +# # SYNOPSIS # smart_exit [-h] [-n] # diff --git a/functions/ssh.fish b/functions/ssh.fish index 162432f..1c80d4f 100644 --- a/functions/ssh.fish +++ b/functions/ssh.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/network # +# CLASSIFICATION +# bypasses-shadow(ssh), network +# # SYNOPSIS # ssh [args...] # diff --git a/functions/superpowers.fish b/functions/superpowers.fish index dbd3781..23443dc 100644 --- a/functions/superpowers.fish +++ b/functions/superpowers.fish @@ -4,6 +4,9 @@ # CATEGORY # 12-ai-and-developer-tools # +# CLASSIFICATION +# uses-shadow(claude) +# # SYNOPSIS # superpowers [on|off] [-g] # diff --git a/functions/top.fish b/functions/top.fish index 73c77df..bf8ae10 100644 --- a/functions/top.fish +++ b/functions/top.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/monitor # +# CLASSIFICATION +# bypasses-shadow(top) +# # SYNOPSIS # top [args...] # diff --git a/functions/upgrade.fish b/functions/upgrade.fish index 9139801..19460d7 100644 --- a/functions/upgrade.fish +++ b/functions/upgrade.fish @@ -7,6 +7,9 @@ # COMPONENT # integrations/pkg-upgrade # +# CLASSIFICATION +# network +# # SYNOPSIS # upgrade # diff --git a/functions/view.fish b/functions/view.fish index fa0f665..e793b6c 100644 --- a/functions/view.fish +++ b/functions/view.fish @@ -4,6 +4,9 @@ # CATEGORY # 03-editors-and-viewers # +# CLASSIFICATION +# uses-shadow(less) +# # SYNOPSIS # view [args...] # diff --git a/functions/yt-dlp.fish b/functions/yt-dlp.fish index 340f7ea..f07a19f 100644 --- a/functions/yt-dlp.fish +++ b/functions/yt-dlp.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/network # +# CLASSIFICATION +# network +# # SYNOPSIS # yt-dlp [args...] URL [URL...] # -- 2.54.0 From 069a1f774330e75b78f63f9923ae7f00ec853f94 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 21 Sep 2026 21:26:47 -0400 Subject: [PATCH 3/7] docs(classification): move schema out of gitignored AGENTS/, into docs/ AGENTS/functions/CLAUDE.md is git-ignored local agent state, not part of the repo -- a comment/commit referencing it as the schema's home points contributors at a file they can't see. The canonical CLASSIFICATION schema now lives at docs/function-classification-schema.md (tracked), with CONTRIBUTING.md's existing function-header-conventions section extended to introduce it, and the C1 shadow doc's pointer updated to match. AGENTS/functions/CLAUDE.md keeps only a one-line pointer to the tracked file instead of duplicating the definitions. --- CONTRIBUTING.md | 12 +++ docs/function-classification-schema.md | 74 +++++++++++++++++++ .../01-c1-command-shadows.md | 5 +- 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 docs/function-classification-schema.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05903bb..8d2c9c3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -391,6 +391,7 @@ all optional except where noted: | `CATEGORY` | **Required to appear in the manual at all** — see below. | | `COMPONENT` | Only for functions gated by the [opinionated-component system](#opinionated-components). | | `DEPENDENCIES` | Other functions this one calls that a reader may want to look up. | +| `CLASSIFICATION` | Hazard/shadow-interaction tags — see below. | | `SYNOPSIS` | One-line usage form. | | `DESCRIPTION` | Prose description; can span multiple paragraphs. | | `ARGUMENTS` | Flags/positional args, one per line. | @@ -414,6 +415,9 @@ A full example (`functions/claude.fish`): # DEPENDENCIES # agents-init # +# CLASSIFICATION +# bypasses-shadow(claude) +# # SYNOPSIS # claude [ARGS...] # @@ -443,6 +447,14 @@ If your function genuinely doesn't fit any of these, add a new `docs/manual/05-functions/NN-your-category.md` stub (with frontmatter matching its siblings) rather than force-fitting it into an existing one. +**`CLASSIFICATION` flags hazards and shadow interactions, optional and +omitted when nothing applies:** whether the function calls a +[C1-shadowed command](docs/manual/08-components-reference/01-c1-command-shadows.md) +bare wanting the override (`uses-shadow(ls)`) or bypasses it deliberately +via `command`/`builtin` (`bypasses-shadow(cat)`), and general hazards — +`destructive`, `network`, `blocking-prompt`. Full tag definitions and +placement rule: [`docs/function-classification-schema.md`](docs/function-classification-schema.md). + ### Private/internal helper functions Functions named with a leading `_` (e.g. `_agents_init_ensure_gitignore`, diff --git a/docs/function-classification-schema.md b/docs/function-classification-schema.md new file mode 100644 index 0000000..57a0039 --- /dev/null +++ b/docs/function-classification-schema.md @@ -0,0 +1,74 @@ +# Function CLASSIFICATION schema + +This is the canonical definition of the `# CLASSIFICATION` function +doc-header label. It's referenced from code comments and commit messages — +link here, not to anything under `AGENTS/` (that tree is git-ignored local +agent state, not part of the repo). + +See [Public function documentation header](../CONTRIBUTING.md#public-function-documentation-header) +in `CONTRIBUTING.md` for where `CLASSIFICATION` fits among the other header +labels, and [C1 — Command Shadows](manual/08-components-reference/01-c1-command-shadows.md) +for the full list of C1-shadowed commands this schema's shadow tags refer to. + +## Format + +Optional. Comma-separated tags from the closed set below, on the indented +body line directly under the label: + +```fish +# CLASSIFICATION +# uses-shadow(ls), destructive +``` + +Omit the label entirely when nothing applies — omission means "nothing to +flag," not "not yet audited," so don't add it speculatively, and don't add +it empty as a placeholder. + +## Tags + +- **`uses-shadow(name[,name...])`** — calls a C1-shadowed command (see the + C1 doc linked above) bare, deliberately wanting the overridden behavior + (e.g. `ls` wanting eza's icons for a human to read). +- **`bypasses-shadow(name[,name...])`** — calls `command `, + `builtin `, or (for `help` specifically) `__original_help $argv`, + deliberately forcing stock behavior because the shadow's override would + break this function's logic: timestamps leaking into a parsed capture, + `-i` prompting on a path meant to run unattended, structural output + changes breaking a `string`/`sed` parse, etc. +- **`destructive`** — can irreversibly delete or overwrite data: `rm -f`, + `rm -rf`, truncating or force-overwriting a file, `git push --force`. + Routine cleanup of the function's own `$tmpdir`/`$_tmpdir`/`mktemp` + output (or other output it just created in this same call) is expected + behavior, not a hazard — don't tag it. +- **`network`** — makes an outbound network call: `curl`, `wget`, `ssh`, + `git fetch`/`pull`/`push`/`clone`, `paru`/`yay` (package-manager network + ops), talking to an API, etc. +- **`blocking-prompt`** — can block waiting on interactive confirmation + with no non-interactive escape hatch: a shadow's forced `-i`, fish's + `read` (genuinely waiting on a terminal — not a `string split | read` + or `while read` consuming a pipe, which never blocks), a `confirm`-style + prompt with no `--yes`/`--force`/`--silent` bypass. Don't tag a function + that's only ever meant to be run interactively at a prompt (a keybinding + handler, an fzf-driven picker) — the hazard this tag exists for is a + script or another function calling it unexpectedly, not a human running + it themselves. + +## Placement + +Directly under `# DEPENDENCIES` if the header has one; otherwise directly +under `# COMPONENT`; otherwise directly under `# CATEGORY`; otherwise as +the first label in the header block (this is the common case for internal +`_`-prefixed helpers, which usually carry none of the three). + +## Judgment calls + +`uses-shadow` vs `bypasses-shadow` is the easiest place to get subtly +wrong — verify against the actual code, not just whether the name appears +in the file. A function that only calls a *helper* which itself interacts +with a shadow does not get the tag; the tag belongs on the helper. When +generating these tags in bulk (e.g. delegating the sweep to another +model), review every result against the source before trusting it — this +schema's own rollout caught several false positives this way: a piped +`read` misread as an interactive prompt, a documented `--yes` flag missed +as an escape hatch, and cleanup of a function's own temp output flagged +as `destructive` despite the explicit exclusion above. diff --git a/docs/manual/08-components-reference/01-c1-command-shadows.md b/docs/manual/08-components-reference/01-c1-command-shadows.md index a71900c..87356a7 100644 --- a/docs/manual/08-components-reference/01-c1-command-shadows.md +++ b/docs/manual/08-components-reference/01-c1-command-shadows.md @@ -108,6 +108,7 @@ toggle state: editor launch. A function's own doc header records which of these it depends on: see the -`CLASSIFICATION` label (`uses-shadow(...)` / `bypasses-shadow(...)`) in -`AGENTS/functions/CLAUDE.md`. +`CLASSIFICATION` label (`uses-shadow(...)` / `bypasses-shadow(...)`), +documented in full at +[`docs/function-classification-schema.md`](../../function-classification-schema.md). -- 2.54.0 From 100cb478bc8059d066b1d72100a0a8da95ab3baf Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 21 Sep 2026 21:26:50 -0400 Subject: [PATCH 4/7] fix(functions): stop leaking scratch files to trash via bare rm Verified an agy audit of every bare rm call (the trash-routing C1 shadow) by hand rather than trusting its report. Confirmed correct: scrub.fish's custom_rm strategy and logs.fish's Ctrl-D delete both deliberately want trash for a real, user-facing deletion. Confirmed and fixed three cases where a function's own throwaway scratch file was going to the user's trash instead of being wiped: fc.fish's edited-command tmpfile, dng2avif.fish's intermediate PNM (inconsistent with its own failure-path cleanup two lines up, which already used -f), and _scrollback_prune_junk.fish's junk log files (its sibling _prune_terminal_logs.fish already documents this exact pitfall in its header). Also went further than the report and classified every bypasses-shadow(rm) caller found by grep that had never been audited at all: config-settings.fish and edit.fish (own scratch cleanup, no destructive data at stake) and key-crypt.fish (--remove deletes the user's real input file after encryption, genuinely destructive, already documented in its own header as 'not a secure wipe'). Corrected scrub.fish's tag, which was missing uses-shadow(rm) for its deliberate trash-routing branch alongside the bypass branch it already had tagged. Added a note to the schema doc: rm's flag-based fallback lives inside the shadow itself, so a bare rm -f/rm -rf call is not the caller bypassing anything -- only an explicit command rm/builtin rm earns the tag. This is why dng2avif.fish's fix needed no CLASSIFICATION change: it already used rm -f, which was never actually the bug -- the missing -f on line 122 was. --- docs/function-classification-schema.md | 7 +++++++ functions/_scrollback_prune_junk.fish | 8 ++++---- functions/config-settings.fish | 3 +++ functions/dng2avif.fish | 2 +- functions/edit.fish | 3 +++ functions/fc.fish | 9 ++++++--- functions/key-crypt.fish | 3 +++ functions/scrub.fish | 2 +- 8 files changed, 28 insertions(+), 9 deletions(-) diff --git a/docs/function-classification-schema.md b/docs/function-classification-schema.md index 57a0039..5bf75a1 100644 --- a/docs/function-classification-schema.md +++ b/docs/function-classification-schema.md @@ -72,3 +72,10 @@ schema's own rollout caught several false positives this way: a piped `read` misread as an interactive prompt, a documented `--yes` flag missed as an escape hatch, and cleanup of a function's own temp output flagged as `destructive` despite the explicit exclusion above. + +`rm` specifically has its own internal flag check (any flag other than +`-r`/`-R`/`--recursive` falls back to `command rm` *inside the shadow +itself*, before it ever touches trash) — a caller writing plain `rm -f` +or `rm -rf` is not bypassing anything itself, the shadow is. Only tag +`bypasses-shadow(rm)` when the caller explicitly writes `command rm` or +`builtin rm`; a bare `rm -f`/`rm -rf` call gets no shadow tag at all. diff --git a/functions/_scrollback_prune_junk.fish b/functions/_scrollback_prune_junk.fish index 4d8b165..022f35b 100644 --- a/functions/_scrollback_prune_junk.fish +++ b/functions/_scrollback_prune_junk.fish @@ -2,7 +2,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later # CLASSIFICATION -# bypasses-shadow(cat) +# bypasses-shadow(cat,rm), destructive # # SYNOPSIS # _scrollback_prune_junk [dir] @@ -29,7 +29,7 @@ function _scrollback_prune_junk --description 'Remove empty, trivial, and Kitty # Remove any completely empty log file regardless of source for f in $dir/*.log $dir/*.txt test -f $f || continue - not test -s $f; and rm $f + not test -s $f; and command rm -f $f end # Remove any log with only a single meaningful line (e.g. [exited], a lone prompt, or a trivial error) @@ -37,7 +37,7 @@ function _scrollback_prune_junk --description 'Remove empty, trivial, and Kitty test -f $f || continue set -l line_count (command cat $f | sed 's/\x1b\[[0-9;:]*[a-zA-Z]//g' | grep -cv '^\s*$') if test $line_count -le 1 - rm $f + command rm -f $f end end @@ -45,7 +45,7 @@ function _scrollback_prune_junk --description 'Remove empty, trivial, and Kitty for f in $dir/scrollback_*.log $dir/scrollback_*.txt test -f $f || continue if command cat $f | sed 's/\x1b\[[0-9;:]*[a-zA-Z]//g' | grep -q 'Enter the new title for this tab below' - rm $f + command rm -f $f end end end diff --git a/functions/config-settings.fish b/functions/config-settings.fish index e873f09..0478ba6 100644 --- a/functions/config-settings.fish +++ b/functions/config-settings.fish @@ -8,6 +8,9 @@ # __fish_palette, __config_settings_state, __config_settings_apply, # __config_settings_set_value, python3 # +# CLASSIFICATION +# bypasses-shadow(rm) +# # SYNOPSIS # config-settings [-h | --help] # diff --git a/functions/dng2avif.fish b/functions/dng2avif.fish index 4185b8f..c435ca5 100644 --- a/functions/dng2avif.fish +++ b/functions/dng2avif.fish @@ -119,7 +119,7 @@ function dng2avif --description 'Convert DNG raw to 10-bit HDR AVIF' end # Final Cleanup - test -f "$temp_pnm"; and rm "$temp_pnm" + test -f "$temp_pnm"; and rm -f "$temp_pnm" set -l size (stat -c '%s' "$output" | numfmt --to=iec) echo (set_color yellow)"Complete: $output ($size)"(set_color normal) diff --git a/functions/edit.fish b/functions/edit.fish index b312b83..155c208 100644 --- a/functions/edit.fish +++ b/functions/edit.fish @@ -7,6 +7,9 @@ # COMPONENT # aliases/dev-tools # +# CLASSIFICATION +# bypasses-shadow(rm) +# # SYNOPSIS # edit [-V|-t] [-e EDITOR] [-c] [-x TEXT] [-n] [-v|-s] [FILE...] # diff --git a/functions/fc.fish b/functions/fc.fish index a65e230..0825ba9 100644 --- a/functions/fc.fish +++ b/functions/fc.fish @@ -4,6 +4,9 @@ # CATEGORY # 03-editors-and-viewers # +# CLASSIFICATION +# bypasses-shadow(rm) +# # SYNOPSIS # fc [command_prefix] # @@ -50,15 +53,15 @@ function fc --description 'Edit and execute the last command (Bash-style fc)' # Final check if user cleared the file in the editor if test -s $tmpfile set -l command (cat $tmpfile) - rm $tmpfile + command rm -f $tmpfile commandline -r "$command" commandline -f execute else - rm $tmpfile + command rm -f $tmpfile echo "fc: Aborted (empty file)" end else - rm $tmpfile + command rm -f $tmpfile echo "fc: Could not retrieve history" end end diff --git a/functions/key-crypt.fish b/functions/key-crypt.fish index 946b876..5bd9dae 100644 --- a/functions/key-crypt.fish +++ b/functions/key-crypt.fish @@ -7,6 +7,9 @@ # DEPENDENCIES # gpg, tar # +# CLASSIFICATION +# bypasses-shadow(rm), destructive +# # SYNOPSIS # key-crypt [options] [output] # key-crypt -i -o [options] diff --git a/functions/scrub.fish b/functions/scrub.fish index 7444c81..d91c36d 100644 --- a/functions/scrub.fish +++ b/functions/scrub.fish @@ -5,7 +5,7 @@ # 01-file-and-directory # # CLASSIFICATION -# bypasses-shadow(rm), destructive +# uses-shadow(rm), bypasses-shadow(rm), destructive # # SYNOPSIS # scrub [-a] [-d] [-h] -- 2.54.0 From 373917d00288a98af97882e093cf050ddc0c8912 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 21 Sep 2026 21:26:52 -0400 Subject: [PATCH 5/7] fix(functions): stop routing deterministic cd through zoxide's shadow Verified an agy audit of every bare cd call by hand. conf.d/zoxide.fish gates alias cd=z behind status is-interactive plus the C1 toggle, and _zoxide_hook fires on --on-variable PWD, so it tracks a directory change no matter how PWD got there -- switching to builtin cd loses zoxide's frecency tracking nothing. mkcd.fish's single cd and mkrep.fish's 9 (entering the new repo, plus 8 rollback-to-original-directory sites on error paths and --no-cd) were both intended as exact, deterministic path navigation, never a zoxide query. The real risk was mkrep's rollback path: if $orig_pwd ever failed cd's own -d check for any reason, z's fallback branch queries zoxide for a *guessed* frecent directory instead -- landing a failed run's cleanup in a directory the caller never asked for, not the one it was trying to return to. All 9 sites now use builtin cd. Corrected both functions' CLASSIFICATION from uses-shadow(cd) to bypasses-shadow(cd) -- neither wanted zoxide's query, they were tagged that way only because the header audit recorded what the code was doing at the time, not what it needed. integrations/fzf.fish's fzf-alt-c-widget also calls bare cd, but it's vendored upstream code (PatrickF1/fzf.fish) and is itself an interactive directory-jump binding, not a script/automation caller -- left alone, same as fisher.fish's rm calls. --- functions/mkcd.fish | 4 ++-- functions/mkrep.fish | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/functions/mkcd.fish b/functions/mkcd.fish index 161a77a..e8173d3 100644 --- a/functions/mkcd.fish +++ b/functions/mkcd.fish @@ -5,7 +5,7 @@ # 01-file-and-directory # # CLASSIFICATION -# uses-shadow(cd) +# bypasses-shadow(cd) # # SYNOPSIS # mkcd [-s | --silent] @@ -68,7 +68,7 @@ function mkcd --description 'Create a directory (with parents) and cd into it' _fish_mkdir_p --tree $dir; or return $status end - cd $dir + builtin cd $dir or return $status if test $is_new -eq 1 diff --git a/functions/mkrep.fish b/functions/mkrep.fish index 725ba33..e12bb51 100644 --- a/functions/mkrep.fish +++ b/functions/mkrep.fish @@ -10,7 +10,7 @@ # _mkrep_repo_exists, git # # CLASSIFICATION -# uses-shadow(cd), destructive, network +# bypasses-shadow(cd), destructive, network # # SYNOPSIS # mkrep [--cd | --no-cd] [--mkdir | --no-mkdir] [--git | --no-git] @@ -307,7 +307,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it' _mkrep_say $silent "$c_warn""→$c_reset $c_arg$dir$c_reset already exists" end - cd $dir + builtin cd $dir or begin echo "$c_err""✘$c_reset Failed to enter $c_arg$dir$c_reset" >&2 return 1 @@ -329,7 +329,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it' end or begin echo "$c_err""✘$c_reset git init failed in $c_arg$dir$c_reset" >&2 - cd $orig_pwd + builtin cd $orig_pwd return 1 end @@ -344,7 +344,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it' _mkrep_verbose $silent $verbose "$c_dim""Running: git remote add origin $_flag_remote$c_reset" _mkrep_add_origin $silent $_flag_remote or begin - cd $orig_pwd + builtin cd $orig_pwd return 1 end end @@ -354,7 +354,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it' test -z "$cmd"; and set cmd $MKREP_REMOTE_CMD if test -z "$cmd" echo "$c_err""✘$c_reset --new-remote given no command and \$MKREP_REMOTE_CMD is unset" >&2 - cd $orig_pwd + builtin cd $orig_pwd return 1 end @@ -371,7 +371,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it' end or begin echo "$c_err""✘$c_reset Remote-create command failed" >&2 - cd $orig_pwd + builtin cd $orig_pwd return 1 end _mkrep_say $silent "$c_ok""✔$c_reset Ran remote-create command" @@ -393,7 +393,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it' _mkrep_say $silent "$c_warn""→$c_reset $c_arg$USER/$name$c_reset already exists on $srv_type; linking instead of creating" _mkrep_add_origin $silent $url or begin - cd $orig_pwd + builtin cd $orig_pwd return 1 end else @@ -424,7 +424,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it' if string match -q '*{server}*' -- $cmd if test -z "$srv_url" echo "$c_err""✘$c_reset No base URL resolved for $srv_type (set \$GITEA_URL/\$GITEA_HOST or \$GITLAB_URL/\$GITLAB_HOST)" >&2 - cd $orig_pwd + builtin cd $orig_pwd return 1 end set cmd (string replace -a '{server}' $srv_url -- $cmd) @@ -440,7 +440,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it' end or begin echo "$c_err""✘$c_reset Remote-create command failed" >&2 - cd $orig_pwd + builtin cd $orig_pwd return 1 end set -l url (_mkrep_remote_url $srv_type $USER $name $srv_url) @@ -451,7 +451,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it' end if test $do_cd -eq 0 - cd $orig_pwd + builtin cd $orig_pwd else _mkrep_say $silent "$c_ok""✔$c_reset Entered $c_arg$dir$c_reset" end -- 2.54.0 From 3414f81cb63d588de60685ad64eae06bb6cbcfbb Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 21 Sep 2026 21:26:55 -0400 Subject: [PATCH 6/7] feat(tests): add shadow-classification lint; fix real cp/mv/less bugs New Phase 1b in tests/run-tests.fish: catches a bare C1-shadowed-command call in a functions/*.fish body with no matching uses-shadow(name) or self-limiting(name) in that function's own CLASSIFICATION header. This is exactly the check discussed after the rm and cd audits -- runtime auto-unwrapping isn't viable in fish (there's no hook finer than shadowing itself, and rewriting behavior invisibly at runtime is its own footgun); a static lint using the CLASSIFICATION tag as the declared-intentional marker is. Scoped to functions/*.fish only: the one-function-per-file convention there makes body extraction exact with no block-depth parser needed. Added a new self-limiting(name) tag to the schema for the case a bare call is safe not because the caller did anything, but because the shadow's own logic already neutralizes the override: rm's and mkdir's flag checks (verified precisely -- rm falls back to command rm for any flag except a bare -r/-R/--recursive alone, which still routes to trash; mkdir falls back to command mkdir -p for any flag, no exception), and grep/fgrep/egrep/dir/vdir/cat's own tty auto-detection (--color=auto, and bat's default color behavior -- verified byte-identical to stock cat when piped, since bat also auto-disables highlighting on a non-terminal). Explicit and durable rather than a silent lint exemption: if a shadow's bypass condition is ever weakened, every self-limiting site is one grep away instead of silently wrong. Running the first draft of the lint surfaced three more real bugs, none previously audited: - config-help.fish's --man pager path checks `type -q less` (proving it wants the real less binary specifically, for less-only -R/+N flag syntax) then called it bare, routing through our own $PAGER -> ov -> less -> more -> cat fallback chain instead -- which could hand those less-specific flags to a completely different program. Now command less. - _fish_deps_install.fish and _fish_deps_update.fish's binary-upgrade paths cp a freshly downloaded binary over an already-installed one with no existence guard -- the update flow's target is guaranteed to already exist. Our cp shadow forces -i unconditionally (a plain alias, not flag-aware like rm's), so this would hang waiting on a confirmation prompt in any non-interactive run. Now command cp. Same two files' lazydocker install path piped curl output into bare bash, invoking our shell-switch wrapper instead of a plain subshell. Now command bash. - agents-init.fish's AGENTS.md/CLAUDE.md relocation calls mv bare in four places; each is already guarded by a preceding test -f check on the destination, so the -i alias was unlikely to ever fire in practice, but explicit command mv removes the reliance on that guard entirely rather than leaving it as the only thing standing between a file move and an unattended hang. The remaining ~65 flagged call sites across ~24 files were reviewed individually and tagged self-limiting(rm)/self-limiting(mkdir) (verified flagged with -f/-rf or -p) and self-limiting(grep)/ self-limiting(cat) (verified piped, captured, or -q/-c; none display color to a human), plus uses-shadow(ls) for two existence-check-only calls (cffetch.fish, ffetch.fish) whose output is redirected to /dev/null. --- docs/function-classification-schema.md | 20 +++++ functions/__fish_config_sync_logging.fish | 3 + functions/__fish_real_command.fish | 3 + functions/__fish_user_dots_link.fish | 2 +- functions/_agents_init_ensure_gitignore.fish | 3 + functions/_agents_repo_ensure_symlink.fish | 3 + functions/_fish_deps_install.fish | 8 +- functions/_fish_deps_marktext_appimage.fish | 4 +- functions/_fish_deps_update.fish | 8 +- functions/_scrollback_prune_junk.fish | 2 +- functions/agents-init.fish | 11 ++- functions/agents-vault.fish | 3 + functions/antigravity-ide.fish | 3 + functions/cffetch.fish | 3 + functions/cleanup.fish | 3 + functions/config-help.fish | 5 +- functions/dng2avif.fish | 3 + functions/fc.fish | 2 +- functions/ffetch.fish | 3 + functions/gi.fish | 2 +- functions/logs.fish | 2 +- functions/mkrep.fish | 2 +- functions/qr.fish | 2 +- functions/sbver.fish | 3 + functions/smart_exit.fish | 2 +- tests/run-tests.fish | 83 ++++++++++++++++++++ 26 files changed, 165 insertions(+), 23 deletions(-) diff --git a/docs/function-classification-schema.md b/docs/function-classification-schema.md index 5bf75a1..9631b44 100644 --- a/docs/function-classification-schema.md +++ b/docs/function-classification-schema.md @@ -35,6 +35,26 @@ it empty as a placeholder. break this function's logic: timestamps leaking into a parsed capture, `-i` prompting on a path meant to run unattended, structural output changes breaking a `string`/`sed` parse, etc. +- **`self-limiting(name[,name...])`** — calls a shadowed command bare, and + it's safe not because the caller did anything but because *the shadow's + own logic* already neutralizes the override for this call. Verify the + actual condition per shadow, it's not the same check for each one: + - `rm` falls back to `command rm` for any flag **except** a bare `-r`, + `-R`, or `--recursive` (those still route to `trash put`) — so + `rm -f`/`rm -rf` qualify, but `rm -r $dir` alone does not. + - `mkdir` falls back to `command mkdir -p` for *any* flag at all, no + exception. + - `--color=auto`/`bat`'s own tty auto-detection (`grep`, `fgrep`, + `egrep`, `dir`, `vdir`, `cat` — verified byte-identical to stock when + piped or captured, since none of these force color on a + non-terminal). + + Document it explicitly rather than leaving the bare call untagged: if a + shadow's bypass condition is ever weakened, narrowed, or removed, every + `self-limiting` site is one grep away instead of silently wrong. + Don't use this for `ls` — eza's long-format/icon layout is structural, + not tty-gated, so it stays different from stock `ls` even piped; a + bare `ls` call still needs `uses-shadow(ls)` or a real bypass. - **`destructive`** — can irreversibly delete or overwrite data: `rm -f`, `rm -rf`, truncating or force-overwriting a file, `git push --force`. Routine cleanup of the function's own `$tmpdir`/`$_tmpdir`/`mktemp` diff --git a/functions/__fish_config_sync_logging.fish b/functions/__fish_config_sync_logging.fish index f515a59..58e1708 100644 --- a/functions/__fish_config_sync_logging.fish +++ b/functions/__fish_config_sync_logging.fish @@ -4,6 +4,9 @@ # COMPONENT # logging/terminal-capture # +# CLASSIFICATION +# self-limiting(rm,mkdir) +# # SYNOPSIS # __fish_config_sync_logging # diff --git a/functions/__fish_real_command.fish b/functions/__fish_real_command.fish index d515c8b..9669ee2 100644 --- a/functions/__fish_real_command.fish +++ b/functions/__fish_real_command.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# self-limiting(grep) +# # SYNOPSIS # __fish_real_command # diff --git a/functions/__fish_user_dots_link.fish b/functions/__fish_user_dots_link.fish index d7026b8..848fd87 100644 --- a/functions/__fish_user_dots_link.fish +++ b/functions/__fish_user_dots_link.fish @@ -5,7 +5,7 @@ # autoexec/sync # # CLASSIFICATION -# destructive +# self-limiting(rm), destructive # # SYNOPSIS # __fish_user_dots_link diff --git a/functions/_agents_init_ensure_gitignore.fish b/functions/_agents_init_ensure_gitignore.fish index f1f5549..ca1d737 100644 --- a/functions/_agents_init_ensure_gitignore.fish +++ b/functions/_agents_init_ensure_gitignore.fish @@ -1,6 +1,9 @@ # Copyright (C) 2026 Rootiest # SPDX-License-Identifier: AGPL-3.0-or-later +# CLASSIFICATION +# self-limiting(grep) +# # SYNOPSIS # _agents_init_ensure_gitignore