fish-shell-scripting — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited fish-shell-scripting (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Related skills:
software-engineer - General scripting design principlespython-programmer - When shell complexity exceeds ~100 lines, consider Python<core_philosophy> Fish is a user-friendly shell that prioritizes correctness over POSIX compatibility. Its lists-not-strings semantics eliminates entire classes of bugs common in bash/zsh. The limitation is portability, not capability.
Key insight: Fish handles sophisticated automation well. Choose Fish when you control the environment; choose bash when you don't. </core_philosophy>
<platform_requirements> Compatibility: All Fish scripts MUST work on both macOS and Fedora Linux. Handle platform differences via uname detection.
Quote paths with spaces: Always quote file paths even though Fish has no word splitting. Fish doesn't need quotes technically (no word splitting), but quoting anyway: (1) makes intent explicit for readability, (2) builds consistent habits when switching between shells, (3) ensures compatibility when passing to external commands that may interpret spaces.
Shebang: Use #!/usr/bin/env fish for portability across installation locations. </platform_requirements>
<fish_vs_alternatives> Use Fish for:
argparse + fish_opt)Use bash for:
/bin/sh compatibilityUse a programming language for:
</fish_vs_alternatives>
<posix_differences> No word splitting: Variables don't split on spaces. set name "foo bar"; echo $name is one argument. This eliminates a major source of bugs in bash/zsh.
All variables are lists: A "string" is a one-element list. Indexing is 1-based: $PATH[1], $PATH[-1].
No `VAR=value` syntax: Use set command. set -gx VAR value (global exported), set -lx VAR value (local exported).
Command substitution splits on newlines only: set lines (cat file) creates one element per line. For space-splitting: string split " ". </posix_differences>
<scoping_decision> Choosing a scope:
| Need | Scope | Example Use Case |
|---|---|---|
| Temporary within function/block | -l | Loop variables, intermediate results |
| Shared across session | -g | Current project settings, temporary overrides |
| Available to child processes | -gx or -lx | PATH, EDITOR, build flags |
| Persist across sessions (Fish UI only) | -U | fish_color_*, key bindings, prompt config |
Flag combinations:
-g or -l)Example: set -gx EDITOR vim (global + exported), set -lx DEBUG 1 (local + exported to children).
CRITICAL: Environment variables (PATH, EDITOR, etc.) should NEVER be universal. Universal scope is for Fish UI config only. </scoping_decision>
<universal_variable_antipattern> NEVER use universal variables for PATH or environment variables. Universal scope is for Fish UI config only (colors, key bindings, prompt).
Why this matters: Universal variables persist to disk and are shared across all Fish sessions. If you append to PATH in config.fish using universal variables, it grows indefinitely on every shell start because:
# WRONG: Grows PATH indefinitely on every shell start
set -U fish_user_paths ~/bin $fish_user_paths
# RIGHT: Session-scoped (recalculated fresh each session)
set -gx PATH ~/bin $PATH
# RIGHT: Use fish_add_path once interactively (idempotent, uses universal internally)
fish_add_path ~/binSafe universal variable uses: fish_color_*, fish_key_bindings, fish_prompt, fish_greeting. </universal_variable_antipattern>
<cross_platform> OS detection: switch (uname); case Darwin; ...; case Linux; ...; end
Conditional PATH: test -d ~/.cargo/bin; and fish_add_path ~/.cargo/bin
Platform-specific utilities to watch for:
sed, find, date, stat, readlink behave differently/usr/local/bin (macOS Homebrew) vs /usr/bin (Fedora)</cross_platform>
<critical_idioms> Wrapper functions require `--wraps` and `$argv`:
function ls --wraps=ls --description "ls with color"
command ls --color=auto $argv
end--wraps=ls: Completions break (Fish doesn't know what command to complete for)$argv: Arguments are swallowed (user's flags/paths ignored)Argument parsing: Use argparse + fish_opt for complex CLI tools. Check flags with set -q _flag_name.
Guard interactive code: Non-interactive shells (SSH, rsync) execute config.fish. Guard output:
if status is-interactive
echo "Welcome!"
endWithout this guard: SSH file transfers, rsync, and scp can fail because unexpected output corrupts the protocol.
Use `string` builtin: Prefer string match, string split over external grep/cut. Benefits: faster execution (no fork), consistent cross-platform behavior, proper exit codes.
and/or for control flow: Chain commands with and/or, not just &&/||. Both work, but and/or are Fish's native idiom. </critical_idioms>
<organization_decisions>
| Content Type | Location | Why |
|---|---|---|
| Functions (reusable commands) | ~/.config/fish/functions/name.fish | Autoloaded on first use, not at startup—keeps shell fast |
| Topical config (per-tool setup) | ~/.config/fish/conf.d/tool.fish | Auto-sourced alphabetically, keeps concerns separated |
| Interactive-only setup | config.fish with status is-interactive | Guards against breaking non-interactive use |
| Environment variables | conf.d/ with set -gx | Session-scoped, recalculated fresh each session |
Minimal `config.fish`: Keep it to ~15 lines of orchestration. Put actual config in conf.d/ files.
Autoload advantage: Functions in functions/ are loaded only when first called, not at startup. This keeps shell startup fast even with many custom functions. </organization_decisions>
<common_mistakes>
<from_bash>
set VAR value. The VAR=value command pattern doesn't exist.set x "foo bar"; echo $x is ONE argument in Fish. This is a feature, not a bug.set -gx VAR value (global exported).(...) for command substitution, not $(...).and/or are the native idiom.test or [...], not bash's [[...]].</from_bash>
<from_zsh>
$array[1] not $array[1] or ${array[1]}.set -U for persistent settings.</from_zsh>
<from_any_shell>
set -gx or one-time fish_add_path.</from_any_shell> </common_mistakes>
<resources> Online Documentation:
Local Documentation:
Typical locations (check these paths directly):
/opt/homebrew/share/doc/fish//usr/local/share/doc/fish//usr/share/doc/fish/Man pages:
/opt/homebrew/share/man/man1/fish*.1/usr/local/share/man/man1/fish*.1/usr/share/man/man1/fish*.1To find dynamically (from Fish shell):
ls "$(dirname (realpath (command -s fish)))/../share/doc/fish"</resources>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.