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.
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.
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.
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).
The tricks.fish C1 history() shadow drops all args and always shows
timestamps. Calling plain history --max 1 hit that shadow, dumping the
entire history with timestamps instead of one plain entry. Use builtin
history --max 1, matching the convention already used by fc.fish,
_fzf_search_history.fish, and bash_expands.fish.
New optional-tier catalog entry, gated on WSL2 detection
(/proc/sys/kernel/osrelease) so it never surfaces on a plain Linux
box's install/sync prompts, only in the informational status
listing. Downloads the x86_64 binary from GitHub releases to
~/.local/bin/win32yank.exe; fish-deps update refreshes an
already-installed copy the same way.
Extracts _fish_clipboard_copy and _fish_clipboard_paste so the
wl-copy/xclip/win32yank fallback chain lives in one place instead of
four near-duplicates. hist now goes through the same chain, so it
also gets the xclip (X11) fallback it was missing before, alongside
win32yank on WSL2.
y, p, paste, and hist now try win32yank.exe after wl-copy/wl-paste
and xclip, so clipboard access works under WSL2 once win32yank is
installed and reachable through WSL interop. Updates the OS
compatibility docs accordingly.
Adds an OS Compatibility section to the README and the Installation
manual page: developed and tested on Arch Linux, macOS and Windows
unsupported, with the specific Linux-only calls that back that claim
(systemd-inhibit, zramctl/swapon, sbctl, wl-copy/xclip with no
pbcopy/clip.exe fallback, GNU-only stat/numfmt flags) and the baseline
packages (git, gpg, tar, coreutils) assumed present rather than tracked
by fish-deps.
Adds os/os-compatibility/operating-system/compatibility/linux/macos/
mac/windows/wsl keywords to fish-config.index so `help config os` and
friends jump straight to it.
Adds two checks to the shared-palette section, extending the existing
colored list with the functions converted in this branch (config-help,
fish-deps, gi, git-clean, mkrep):
- Every listed function's --help must contain the session's own c_head
and c_cmd escapes -- not just some escape sequence, which the existing
presence check already covered but which a wrong-role bug (still some
color, just not the right one) sails through undetected.
- Every escape sequence found in --help output must be one of
c_reset/c_head/c_cmd/c_flag/c_arg/c_dim. c_warn/c_err/c_ok/c_accent/
c_sel/c_hi are legitimate elsewhere but never in --help text; reaching
for one there is always a bug, which is exactly what logs and
smart_exit did (static c_accent green for the command name).
Verified against the pre-fix smart_exit.fish (58d5cf3~1): both checks
fail on it -- missing c_head on "Options:", and a disallowed \e[32m for
the command name -- confirming this isn't a tautological pass.
Both had headings (Usage:/Options:/Keys in fzf:) left completely
uncolored, flags rendered in the argument color instead of the flag
color, and the command name hardcoded to a static ANSI green (c_accent)
instead of the theme-derived c_cmd -- the last of the raw, unconverted
color usage the audit turned up.
__fish_palette's c_cmd/c_flag/c_arg/c_dim now read fish's own highlighter
variables (fish_color_command/option/param/autosuggestion) when set, and
c_head reads the pager's fish_pager_color_prefix, so --help text matches
whatever the user's actual theme renders at the prompt instead of a fixed
guess at it. Each role keeps its previous static value as a fallback
(e.g. a --no-config script, or the theme variable being unset or empty).
Routes __fish_help_header's title/section colors and config-help's,
gi's, and git-clean's own hand-rolled --help blocks through the palette
instead of their hardcoded set_color calls, which fixes the
--help-vs-actual-prompt color mismatch across most of the config's
functions in one place (__fish_help_header backs the majority of
user-facing --help output).
Also fixes role mismatches found along the way: several already-palette
functions (dockup, fish-deps, pkg, play-media, dng2avif) were coloring
their <placeholder>/[bracket] argument text with c_dim instead of c_arg,
and scrub's "Aggressive Targets:" heading was still a hardcoded
set_color call sitting next to an otherwise-converted help block.
Bring key-crypt in from ~/scripts as functions/key-crypt.fish instead of a
standalone --no-config script. Converts exit-based control flow to return,
drops the fish_exit/signal traps (global-scope hijack risk in a shared
shell) in favor of an outer wrapper that cleans up the temp file and all
scratch globals after every call, and routes rm/mv/mkdir through `command`
so config-level wrapper functions can't intercept them.
--install now writes a small standalone wrapper script (source this
function file, call key-crypt) to ~/.local/bin/key-crypt instead of
copying the file itself, since a fish function can't be exec'd directly by
the .desktop Open With entries. --uninstall is unchanged aside from the
command-prefixed rm.
Both link paths -- --remote and the --server/$GIT_SERVER link-existing
branch -- called `git remote add origin` bare. That fails with "remote
origin already exists" whenever the target is already linked, which is
the normal case for `mkrep .` against an existing checkout and for any
rerun against the same target. The failure took the whole call down with
exit 1.
Route both through a new _mkrep_add_origin helper: add when there is no
origin, accept and report when origin already points at the requested
URL, and refuse when it points elsewhere. A different URL is a different
repo, so repointing stays the caller's explicit decision rather than a
silent rewrite of a checkout mkrep may have been aimed at by mistake.
The gitignore-anchoring check quoted arguments fish_indent removes, so
run-tests.fish failed its indent lint and exited 1 on main even though all
753 assertions passed.
The non-git fallback used a substring match, so a negation line such as
!AGENTS/foo counted as the pattern AGENTS/ already being ignored and the
real rule was never appended.
Resolving the root to (pwd) meant running an agent CLI anywhere created an
AGENTS/ git repo, an AGENTS.md, two root symlinks, and a docs/ tree in that
directory. Scaffolding now requires a git repo or a pre-existing agent file
(AGENTS.md, CLAUDE.md, or AGENTS/); elsewhere it is a no-op.
Two related defects on the `$GIT_SERVER` auto-create path added in #150.
The test suite was creating repositories on a live forge. mkrep resolves a
server from `$GIT_SERVER` plus `$GITEA_URL`/`$GITEA_HOST`, and this repo
doubles as a real ~/.config/fish where all of them are exported, so the six
sections that call a bare `mkrep <dir>` took the auto-create branch and
contacted the server. That is how an empty `rootiest/repo` came to exist on
git.rootiest.dev on 2026-09-14 (since deleted). It also explains why those
cases looked flaky rather than broken: standalone they passed, because the
repo existed and mkrep linked instead of creating, while under
run-tests.fish they failed with `Error: no available login` — a throwaway
XDG_CONFIG_HOME leaves `tea` with no credentials. Passing for that reason is
worse than failing.
The suite now neutralizes all five variables once at setup, the same
shadow-to-empty pattern already used per-section for --check-existing, whose
comment flagged this exact hazard. Nothing depended on the ambient value:
every section wanting a server sets its own `set -lx GIT_SERVER`, and one
section asserts the opposite outright.
mkrep itself now confirms before an implicit create. Creating a repository
on a forge is the only outward-facing thing mkrep does, and on this path an
exported variable is all it takes to reach it, so `mkrep foo` — which reads
as purely local — would make a repo on a server without saying so. It asks
first, defaulting to no; declining leaves the local repo with no remote and
still exits 0. Provenance is what gates the prompt, not the resolved value:
--server, --remote and --new-remote all state outright what they will do, so
none of them prompts, and --yes skips the question. Where it cannot be asked
(a script, a pipe, any non-interactive shell) creation is skipped rather than
assumed, with a note on stderr naming the flags that would allow it.
Nine new assertions cover the skip, its stderr note, --yes, and --server not
prompting. The interactive read is verified by hand under a PTY, both
answers, but is not in the suite: that needs a pseudo-terminal, and the
answer parsing it guards is a single `string match`.
Both back the new `md` wrapper and nothing else, so both land in the
Optional tier, skipped by `fish-deps install`/`sync` unless `--optional`
(or `--all`) is passed.
firejail is a plain system package everywhere, so it needs no special
handling. marktext is not: upstream ships an AUR package and its own
GitHub release assets, and no distro carries it under a common name. Its
`_fdc_pm` entry is therefore deliberately empty, and a `marktext-release`
special offers the AUR package via paru/yay where one is present and
otherwise installs upstream's AppImage to ~/.local/bin/marktext.
The release assets embed their version in the filename, so there is no
stable /releases/latest/download URL to fetch -- `_fish_deps_marktext_appimage`
reads the download URL from the GitHub API instead. Upstream builds the
Linux AppImage for x86_64 only, and the helper says so rather than
downloading an unusable binary.
`fish-deps update` refreshes marktext through the AUR where available, and
otherwise only when ~/.local/bin/marktext exists -- a distro-packaged
marktext belongs to that package manager, and dropping an AppImage into
~/.local/bin would shadow it.
`md` forwards every argument to marktext untouched except two flags of its
own: `--read-only`/`-r` and `--foreground`. By default it detaches via
`bkg`, so the shell stays usable and the editor outlives the window that
launched it.
MarkText has no read-only mode, so `-r` sandboxes it with firejail, binding
each named file read-only. The subtlety is that MarkText is single-instance:
a plain launch hands the file to an already-running, unsandboxed, writable
window and exits, silently defeating the sandbox. `-r` therefore also passes
a private `--user-data-dir`, which forces an independent instance the
read-only bind actually covers, plus `--no-sandbox`, since Electron's own
sandbox needs the user namespaces firejail has already taken away.
Flags whose entire purpose is terminal output (`--version`, `-v`/`--verbose`,
`--debug`) imply `--foreground`; backgrounding them would send the output
you asked for to /dev/null.
The function is autoloaded and so never shadows an `md` function or alias
defined elsewhere -- fish only looks in functions/ when nothing named `md`
exists. A real `md` *binary* would be shadowed, so the body hands off to it
verbatim whenever marktext is not installed. No conf.d file and no
opinionated guard: `md` is a novel name rather than a command shadow, the
same as `bkg` and `detach`.