Fish Scripting: Plugin Management Complete Guide
Fish shell (Friendly Interactive SHell) has earned a loyal following among developers for its rich autocompletion, vibrant syntax highlighting, and sane defaults. As your Fish usage grows, you'll inevitably want to extend its capabilities. This is where plugin management enters the picture — a systematic way to install, organize, update, and share Fish scripts, functions, completions, and themes without cluttering your configuration or manually sourcing dozens of files.
What Is Fish Plugin Management?
Fish plugin management refers to the tools and workflows used to handle reusable Fish code modules — called plugins — that extend the shell's behavior. A plugin can be anything from a simple collection of aliases and functions to a full theme that transforms your prompt, or a completion pack that adds tab-completion support for a specific CLI tool like kubectl or docker.
Plugin managers automate these tasks:
- Fetching plugins from remote repositories (GitHub, GitLab, or local directories)
- Installing them into a designated plugin directory
- Sourcing the plugin code on shell startup so functions and completions become available immediately
- Updating plugins to their latest versions with a single command
- Removing plugins cleanly, without leaving orphaned files behind
Why Plugin Management Matters
Without a plugin manager, you would need to manually clone repositories, copy files into your ~/.config/fish/functions/ or ~/.config/fish/completions/ directories, and maintain separate sourcing logic in your config.fish. This quickly becomes unwieldy when you have more than a handful of plugins. A proper plugin manager gives you:
- Declarative configuration — list your plugins once and the manager handles the rest
- Lazy loading — some managers support loading plugins only when needed, reducing shell startup time
- Version pinning — lock plugins to specific tags or commits for reproducible environments
- Dependency resolution — automatically install plugins that other plugins depend on
- Community ecosystem — easily discover and share plugins with other Fish users
Popular Fish Plugin Managers
There are several mature plugin managers for Fish. Here are the most widely used options:
Fisher — The most popular and actively maintained plugin manager. It's minimal, fast, and uses a flat file (~/.config/fish/fish_plugins) to track installed plugins. Fisher supports plugins from GitHub, GitLab, local paths, and even gists.
Oh My Fish (omf) — Inspired by Oh My Zsh, OMF provides a plugin and theme ecosystem with a CLI tool for management. It uses a structured directory under ~/.local/share/omf/ and maintains separate plugin and theme registries.
Fundle — A zero-dependency plugin manager that works entirely within Fish, without requiring any external installation step. You simply source the fundle script in your config.fish and declare plugins inline.
Manual management — For minimalists, Fish's native autoloading of functions placed in ~/.config/fish/functions/ and ~/.config/fish/completions/ can serve as a lightweight alternative, especially when combined with Git submodules.
Installing Fisher
Fisher can be installed with a single curl command. It places itself in your Fish configuration and adds a fisher function that manages everything else:
# Install Fisher via curl
curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source && fisher install jorgebucaran/fisher
After installation, Fisher lives at ~/.config/fish/functions/fisher.fish and tracks plugins in ~/.config/fish/fish_plugins. You can verify the installation with:
fisher --version
fisher list
Using Fisher: Complete Walkthrough
Fisher's workflow revolves around a few simple commands. Here is a practical end-to-end session:
Installing Plugins
# Install a plugin from GitHub (using shorthand owner/repo)
fisher install jorgebucaran/nvm.fish
# Install multiple plugins at once
fisher install jethrokuan/z jorgebucaran/spark jorgebucaran/hydro
# Install from a local directory
fisher install ~/my-custom-plugin
# Install from a GitLab repository
fisher install gitlab.com/owner/repo
Listing and Browsing Installed Plugins
# List all installed plugins
fisher list
# List with extra details (timestamps, sources)
fisher list --verbose
# Search the Fisher ecosystem for plugins
fisher search nvm
fisher search prompt
Updating Plugins
# Update all plugins to their latest versions
fisher update
# Update a specific plugin
fisher update jethrokuan/z
# Update Fisher itself
fisher update fisher
Removing Plugins
# Remove a plugin
fisher remove jorgebucaran/nvm.fish
# Remove multiple plugins
fisher remove jethrokuan/z jorgebucaran/spark
# The fish_plugins file is automatically updated
Declarative Plugin Management with Fisher
The file ~/.config/fish/fish_plugins is a plain-text list of plugins, one per line. You can edit it directly:
# Example ~/.config/fish/fish_plugins
jorgebucaran/nvm.fish
jethrokuan/z
jorgebucaran/hydro
patrickf1/colored_man_pages.fish
edc/bass
After editing the file, run fisher update to sync your installation with the list. This declarative approach makes it easy to version-control your Fish plugins alongside your dotfiles.
Using Oh My Fish: Complete Walkthrough
Oh My Fish provides a structured plugin and theme system. Install it with:
# Install Oh My Fish
curl -L https://get.oh-my.fish | fish
OMF installs to ~/.local/share/omf/ and adds initialization code to your config.fish. It comes with a CLI called omf:
Managing Plugins with OMF
# List available plugins from the registry
omf search plugin
# Install a plugin
omf install bass
# List installed plugins
omf list
# Remove a plugin
omf remove bass
Managing Themes with OMF
# List available themes
omf theme
# Install and switch to a theme
omf install theme bobthefish
omf theme bobthefish
# See current theme
omf theme --current
# Switch back to default
omf theme default
Updating OMF and Plugins
# Update Oh My Fish itself
omf update
# Update all plugins and themes
omf update --all
# OMF also supports self-updating plugins on reload
omf reload
OMF Plugin Structure
OMF plugins live under ~/.local/share/omf/pkg/ and follow a specific directory convention. A typical OMF plugin looks like this:
~/.local/share/omf/pkg/myplugin/
├── init.fish # Sourced first on plugin load
├── functions/ # Autoloaded Fish functions
│ └── my_helper.fish
├── completions/ # Custom completion definitions
│ └── my_tool.fish
├── conf.d/ # Configuration snippets sourced at startup
│ └── settings.fish
└── README.md
Using Fundle: Zero-Dependency Plugin Management
Fundle requires no installation — you embed it directly in your config.fish. Download the fundle script or source it inline:
# Add this to your ~/.config/fish/config.fish
# Source fundle (download once and keep in your config directory)
if not functions -q fundle
eval (curl -sL https://git.io/fundle)
end
# Declare your plugins
fundle plugin 'jorgebucaran/nvm.fish'
fundle plugin 'jethrokuan/z'
fundle plugin 'edc/bass'
# Initialize fundle — this clones missing plugins and sources them
fundle init
Fundle commands:
# Install all declared plugins (clone repositories)
fundle install
# Update all plugins
fundle update
# List plugins and their versions
fundle list
# Remove a plugin (delete its declaration line and run fundle install)
# Then manually remove the plugin line from config.fish
Manual Plugin Management with Fish Autoloading
Fish's native autoloading mechanism can serve as a lightweight plugin system. Functions placed in specific directories are automatically available without explicit sourcing:
# Fish autoload paths (Fish searches these recursively)
# ~/.config/fish/functions/ — User functions
# ~/.config/fish/completions/ — User completion definitions
# ~/.config/fish/conf.d/ — Snippets sourced at startup
# Example: manually installing a "plugin" with Git submodules
cd ~/.config/fish
git submodule add https://github.com/jethrokuan/z.git functions/z
git submodule add https://github.com/jorgebucaran/nvm.fish.git functions/nvm
# Fish will now autoload z.fish and nvm.fish functions automatically
This approach works well for small setups but lacks update management, dependency tracking, and clean removal. It's best paired with a dotfile manager like chezmoi or yadm.
Creating Your Own Fish Plugins
Writing a Fish plugin is straightforward. The key is structuring your repository so that plugin managers can automatically load its components. Here's a minimal Fisher-compatible plugin:
# Directory structure for a Fisher plugin (e.g., ~/my-fish-plugin/)
my-fish-plugin/
├── functions/
│ └── hello.fish # Function that Fish will autoload
├── completions/
│ └── hello.fish # Completion definitions for the hello command
├── conf.d/
│ └── hello-config.fish # Sourced on startup (set variables, key bindings)
└── README.md
Example plugin code — functions/hello.fish:
function hello --description "Print a friendly greeting"
set -l name $argv[1]
if test -z "$name"
set name "World"
end
echo "Hello, $name! Welcome to Fish shell."
end
Example completion — completions/hello.fish:
complete -c hello -a "(fish_print_hostnames)" -d "Hostname"
complete -c hello -a "World Fish Developer" -d "Common names"
Example startup configuration — conf.d/hello-config.fish:
# Set a global variable for the hello plugin
set -g hello_default_name "World"
set -g hello_enable_colors true
To make this plugin installable via Fisher, push it to GitHub. Users can then run:
fisher install yourusername/my-fish-plugin
For OMF compatibility, add an init.fish at the root that sources your plugin's main logic, and register the plugin with the OMF registry.
Plugin Directory Structure Reference
Regardless of which manager you use, Fish recognizes several special directories within a plugin:
functions/— Functions placed here are autoloaded by Fish on first invocation. No manual sourcing needed. Files should follow the naming conventionfunction_name.fish.completions/— Completion scripts for external commands. These are loaded when the corresponding command is typed.conf.d/— Files here are sourced at the end of Fish startup, afterconfig.fish. Perfect for setting variables, key bindings, or initialization logic.init.fish— A convention used by OMF (and some others) to explicitly source plugin code. Fisher does not require this file but respects it if present.
Best Practices for Plugin Management
- Keep your plugin list lean. Each plugin adds startup overhead and potential conflicts. Before installing, ask whether you truly need it or if a simple alias/function in your
config.fishwould suffice. - Version-control your plugin list. For Fisher, commit
~/.config/fish/fish_pluginsto your dotfiles repository. For Fundle, commit your entireconfig.fish. This makes environment reproduction trivial. - Pin critical plugins to specific versions. If a plugin is essential to your workflow, use Fisher's ability to target a specific tag or commit:
fisher install owner/repo@v1.2.3. - Test plugins before committing. Install plugins in a temporary Fish instance to verify they don't break your shell or cause slow startups:
fish --init-command="fisher install test-plugin; fish -c 'echo ok'". - Keep plugins updated but cautious. Run
fisher updateregularly, but after a major update, restart your shell and check for errors. Some plugin authors introduce breaking changes. - Use lazy loading when possible. Fisher's event-based loading defers plugin initialization until needed. Plugins like
jethrokuan/zload only when you actually use thezcommand. - Separate concerns. Avoid plugins that do too many things. Prefer focused plugins — one for prompt styling, one for Node version management, one for Git aliases — rather than monolithic "framework" plugins.
- Write plugins with portability in mind. Use
#!/usr/bin/env fishshebangs, avoid bash-specific syntax, and test on multiple operating systems if you plan to share your plugin.
Debugging and Troubleshooting Plugins
When plugins misbehave, Fish provides built-in debugging tools:
# Start Fish in debug mode to see what's being sourced
fish --debug-level=3 --debug-output=/tmp/fish_debug.log
# Check which files Fish is loading at startup
fish --print-rusage-self -c "exit 0" 2>&1
# Profile startup time to identify slow plugins
fish --profile /tmp/fish_profile.txt -c "exit 0"
# Then analyze the profile output
sort -t' ' -k2 -nr /tmp/fish_profile.txt | head -20
# List all sourced files during startup
set -g fish_trace 1
# Then start a new Fish session and examine the output
Common issues and their solutions:
# Problem: Plugin functions not available
# Solution: Verify the plugin is installed and functions are in the correct directory
fisher list | grep plugin-name
ls ~/.config/fish/functions/plugin-name*
# Problem: Slow shell startup
# Solution: Profile startup and identify heavy plugins
fish --profile /tmp/profile.txt -c exit
# Consider lazy-loading alternatives or removing unnecessary plugins
# Problem: Conflicting key bindings between plugins
# Solution: Inspect active key bindings
fish_key_reader
bind --all
# Disable problematic bindings in your config.fish after plugin load
# Problem: Plugin works in interactive shell but not in scripts
# Solution: Ensure scripts include proper initialization
# Scripts don't source config.fish by default
# Use 'fish --init-command="source ~/.config/fish/config.fish" script.fish'
Migrating Between Plugin Managers
If you decide to switch from one manager to another, follow a clean migration path:
# Migrating from OMF to Fisher
# 1. Export your current OMF plugin list
omf list --plugin > /tmp/omf_plugins.txt
# 2. Remove OMF initialization from config.fish
# Delete or comment out the OMF block in ~/.config/fish/config.fish
# 3. Remove OMF directory
rm -rf ~/.local/share/omf
# 4. Install Fisher
curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source && fisher install jorgebucaran/fisher
# 5. Reinstall plugins with Fisher
cat /tmp/omf_plugins.txt | xargs -I{} fisher install {}
# Migrating from Fundle to Fisher
# 1. Copy your fundle plugin declarations from config.fish
# 2. Remove fundle blocks from config.fish
# 3. Install Fisher and add plugins to fish_plugins
fisher install plugin1 plugin2 plugin3
# 4. Verify with 'fisher list'
Advanced Fisher Features
Fisher offers several advanced capabilities beyond basic install/remove/update:
# Install from specific Git references
fisher install owner/repo@v2.0.0 # Tag
fisher install owner/repo@abc123def # Commit hash
fisher install owner/repo@main # Branch
# Install from non-GitHub sources
fisher install gitlab.com/owner/repo
fisher install gist.github.com/owner/abc123
fisher install ~/local/path/to/plugin
# Fisher events — run code when plugins load
# In your config.fish:
fisher --on-event myplugin_load --command "echo 'myplugin is ready!'"
# Check for conflicts between plugins
fisher list --conflict
# Generate a clean snapshot of your plugin state
fisher list --bare > ~/.config/fish/fish_plugins_backup
Security Considerations
Fish plugins execute arbitrary code in your shell environment. Exercise the same caution you would with any third-party code:
- Review plugin source code before installing, especially for plugins with few stars or contributors
- Check the plugin's permissions — some plugins may modify your PATH, override critical functions, or make network requests
- Prefer plugins from established maintainers like jorgebucaran, jethrokuan, or edc, whose repositories are widely audited by the community
- Use Fisher's commit pinning to lock plugins to audited versions:
fisher install owner/repo@<audited-commit-hash> - Isolate experimental plugins by testing them in a Docker container or a temporary Fish session before adding them to your daily driver
Performance Optimization
Shell startup time matters, especially when you open many terminal tabs throughout the day. Here are strategies to keep Fish fast despite having plugins:
# Benchmark your current startup time
time fish -c "exit 0"
# Disable all plugins temporarily to measure baseline
mv ~/.config/fish/fish_plugins ~/.config/fish/fish_plugins.bak
time fish -c "exit 0"
# Restore plugins
mv ~/.config/fish/fish_plugins.bak ~/.config/fish/fish_plugins
# Identify slow plugins by enabling them one at a time
# or by profiling:
fish --profile /tmp/fish_profile.txt -c "exit 0"
# Lazy-load plugins that don't need immediate initialization
# Fisher automatically defers loading for plugins using autoloading
# For conf.d scripts, consider wrapping in event handlers:
# In conf.d/myplugin.fish:
function __lazy_load_myplugin --on-event myplugin_init
# heavy initialization here
end
Many modern Fish plugins are designed with lazy loading in mind. For instance, nvm.fish only activates Node version management when you actually invoke the nvm command, not at shell startup. Prefer these well-designed plugins over ones that eagerly initialize.
Conclusion
Fish plugin management transforms a good shell into a personalized powerhouse. Whether you choose Fisher for its speed and simplicity, Oh My Fish for its theme ecosystem, Fundle for its zero-install philosophy, or a manual approach for ultimate control, the key is understanding the underlying mechanisms: Fish's autoloading functions directory, completion system, and conf.d startup snippets. With a clean plugin list, version-controlled configuration, and regular updates, you can maintain a Fish environment that stays fast, portable, and tailored precisely to your workflow. The ecosystem continues to grow — new plugins appear weekly on GitHub, and the tooling around Fisher in particular keeps improving. Start with a minimal set of plugins, master the commands covered in this guide, and let your Fish shell evolve naturally as your needs expand.