← Back to DevBytes

Fish Scripting: Plugin Management Complete Guide

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:

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:

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:

Best Practices for Plugin Management

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:

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles