A smarter cd command - directory jumper using frecency
(noun): directed momentum
Type directory fragments, land where you meant. Gump learns your navigation habits using frecency (frequency + recency) and finds directories with fuzzy matching. The signature feature: no command prefix required. Just type the directory name and press Enter.
Type directory names directly - no g, z, or j needed. Just hit Enter.
Combines frequency and recency with time-based multipliers for intelligent ranking.
Powered by the same engine as Helix editor for fast, accurate fuzzy search.
Matches against current directory contents before querying the database.
Search with multiple terms in order: "conf fish" โ ~/.config/fish
Use gi for interactive directory selection with fzf integration.
Migrate from zoxide, autojump, z, z.lua, zsh-z, or fasd seamlessly.
Bash, Zsh, and Fish with native hooks and keybindings.
After installation, add the initialization to your shell configuration. This enables both directory tracking and no-prefix jumping.
Gump learns your navigation habits automatically. Just use your shell normally:
If you're switching from zoxide, autojump, z, or fasd, import your existing history:
Scores are normalized to prevent imported entries from dominating the database. Only existing directories are imported.
Gump is configured through environment variables. No configuration file is needed - just export the variables in your shell's rc file before the gump init line.
| Option | Type | Default | Description |
|---|---|---|---|
GUMP_DATA_DIR | path | ~/.local/share/gump | Database directory location (XDG-compliant default) |
GUMP_MAXAGE | int | 10000 | Maximum total score before aging is applied |
GUMP_EXCLUDE | string | - | Colon-separated paths to exclude from tracking |
# Custom database location
export GUMP_DATA_DIR="$HOME/.gump"
# Lower max age for faster aging (default: 10000)
export GUMP_MAXAGE="5000"
# Exclude directories from tracking
export GUMP_EXCLUDE="/tmp:/var/tmp:$HOME/.cache:$HOME/Downloads"
# Initialize gump AFTER setting environment variables
eval "$(gump init bash)"
The gump init command
accepts several options to customize behavior:
| Option | Type | Default | Description |
|---|---|---|---|
--cmd <name> | string | g | Custom command name (e.g., j, z, cd) |
--hook <mode> | prompt|pwd | pwd | When to update the database |
--no-cmd | flag | false | Skip g/gi aliases, keep no-prefix jumping only |
Only updates the database when the working directory changes. More efficient for systems with slow disks or frequent prompt renders. Recommended for most users.
Updates the database on every prompt. Catches directories visited via external programs or scripts. Slight performance impact.
By default, gump stores its database following XDG Base Directory Specification:
~/.local/share/gump/db.bin~/Library/Application Support/gump/db.bin$GUMP_DATA_DIR/db.bin
Use GUMP_EXCLUDE to
prevent certain directories from being tracked:
# Exclude temp directories and caches
export GUMP_EXCLUDE="/tmp:/var/tmp:$HOME/.cache:$HOME/.cargo/registry"
# Exclude work directories with sensitive data
export GUMP_EXCLUDE="$GUMP_EXCLUDE:/work/confidential:/opt/secrets"
Any directory that starts with an excluded path will not be tracked. The check uses prefix
matching, so excluding /tmp will exclude
/tmp/foo/bar as well.
Gump is a directory jumper that learns your navigation habits. Unlike traditional cd, gump uses frecency (a combination of frequency and recency) to predict which directory you want. Just type fragments of the path and gump figures out the rest.
Most directory jumpers require you to type a command like z,
j, or autojump
before your query. Gump intercepts unknown commands at the shell level, so you can type:
Rust with nucleo for fuzzy matching, bincode for fast binary serialization, chrono for time handling, and clap for CLI parsing. Release builds are optimized with LTO and single codegen unit for minimal binary size.
Gump operates by tracking your directory changes and intercepting commands that don't exist. When you type something that's not a command, alias, or builtin, gump checks if it's a directory you've visited before.
User types: "proj app"
โ
Shell Integration Layer
โโโ Is "proj" a command/alias/builtin? โ Execute normally
โโโ Is "proj" an exact local directory? โ cd directly
โโโ Query gump
โ
Resolution Pipeline
โโโ 1. Fuzzy match against CWD contents
โ (matches directory NAMES only, not full paths)
โโโ 2. Fuzzy match against frecency database
โ
Fuzzy Matcher (nucleo)
โโโ Score each path against all terms
โโโ Apply term ordering penalties
โโโ Apply last-component boosts
โโโ Combine with frecency bonus
โ
Return best match โ cd to result When you type a command that doesn't exist, gump checks in this strict order:
If it exists, execute normally. Gump never interferes with real commands.
If ./foo exists, cd to it immediately without fuzzy matching.
Search current directory contents. Matches directory names only, not full paths.
Search the frecency database for the best match across all tracked directories.
Fall through to shell's default error if nothing matches.
This order ensures local directories always take precedence, preventing confusion when working in new directories that share names with frequently-visited ones.
The signature feature of gump: just type directory names without any prefix command.
This works by intercepting commands before execution:
command_not_found_handle functionaccept-line\r, \n) to a custom functionExisting commands, aliases, functions, and builtins are never affected. Gump only activates when no matching command exists.
Gump first checks if your query matches something in the current directory:
Important: CWD matching only considers directory names, not full paths. This prevents false positives where a parent directory name matches your query.
# CWD is /home/user/code
$ ls
myapp/ webapp/ utils/
# Query "app"
# CWD mode matches against: "myapp", "webapp", "utils"
# NOT against: "/home/user/code/myapp", etc.
# This prevents "code" from matching all three due to the parent path Gump provides native integration for Bash, Zsh, and Fish. Each shell uses its idiomatic mechanisms for tracking and command interception.
Bash integration uses PROMPT_COMMAND for tracking and command_not_found_handle for no-prefix jumping.
# Tracking hook (pwd mode - only on directory change)
__gump_oldpwd="$PWD"
__gump_pwd_hook() {
if [[ "$PWD" != "$__gump_oldpwd" ]]; then
__gump_oldpwd="$PWD"
command gump add -- "$PWD"
fi
}
PROMPT_COMMAND="__gump_pwd_hook;$PROMPT_COMMAND"
# Jump function (g command)
g() {
if [[ $# -eq 0 ]]; then
builtin cd ~ && __gump_hook
elif [[ $# -eq 1 && "$1" == "-" ]]; then
builtin cd - && __gump_hook
else
local result
result=$(command gump query --cwd -- "$@" 2>/dev/null)
if [[ -z "$result" ]]; then
result=$(command gump query -- "$@" 2>/dev/null)
fi
if [[ -n "$result" ]]; then
builtin cd -- "$result" && __gump_hook
else
echo "gump: no match found" >&2
return 1
fi
fi
}
# No-prefix jumping
command_not_found_handle() {
# 1. Check local directory
# 2. Query CWD
# 3. Query database
# 4. Fall through to error
} Zsh integration uses chpwd hooks for tracking and a ZLE widget for command interception.
# Tracking hook (pwd mode - chpwd hook)
__gump_hook() {
command gump add -- "$PWD"
}
chpwd_functions+=(__gump_hook)
# ZLE widget - intercepts before execution
__gump_accept_line() {
local first_word="${BUFFER%% *}"
# Skip if empty or command exists
if [[ -z "$BUFFER" ]] || whence "$first_word" >/dev/null 2>&1; then
zle .accept-line
return
fi
# Check local dir, then CWD, then database
if [[ -d "$first_word" ]]; then
BUFFER="cd ${(q)first_word}"
else
local result
result=$(command gump query --cwd -- $=BUFFER 2>/dev/null)
if [[ -z "$result" ]]; then
result=$(command gump query -- $=BUFFER 2>/dev/null)
fi
if [[ -n "$result" ]]; then
BUFFER="cd ${(q)result}"
fi
fi
zle .accept-line
}
zle -N accept-line __gump_accept_line Fish integration uses PWD variable hooks for tracking and Enter key binding for command interception.
# Tracking hook (pwd mode - PWD variable)
function __gump_hook --on-variable PWD
command gump add -- $PWD
end
# Jump function
function g --description "Jump to a directory"
if test (count $argv) -eq 0
cd ~
else if test (count $argv) -eq 1 -a "$argv[1]" = "-"
cd -
else
set -l result (command gump query --cwd -- $argv 2>/dev/null)
if test -z "$result"
set result (command gump query -- $argv 2>/dev/null)
end
if test -n "$result"
cd $result
else
echo "gump: no match found" >&2
return 1
end
end
end
# No-prefix jumping via Enter key
function __gump_execute
set -l cmd (commandline -b)
set -l first_word (string split ' ' -- $cmd)[1]
# Skip if empty or command exists
if test -z "$first_word"; or type -q "$first_word"
commandline -f execute
return
end
# Check local, CWD, database
# Rewrite commandline if match found
end
bind \r __gump_execute
bind \n __gump_execute Gump provides several commands for managing the frecency database:
The clean command removes entries for directories that no longer exist AND are older than 90 days. This prevents accidental removal of directories on unmounted drives.
The edit command exports the database to JSON, opens your editor, then imports changes back. Useful for bulk operations or debugging.
Gump can import your history from other directory jumpers. Scores are normalized to prevent imported entries from dominating the database.
| Option | Type | Default | Description |
|---|---|---|---|
zoxide | CLI | zoxide query --list --score | Uses zoxide CLI to export entries with scores |
autojump | file | ~/.local/share/autojump/autojump.txt | Tab-separated score and path format |
z/z.lua/zsh-z | file | ~/.z or $Z_DATA | Pipe-separated path|score|timestamp format |
fasd | file | ~/.fasd | Directories only (fasd tracks files too) |
The import process:
Interactive mode requires fzf to be installed and in PATH.
Use gump query in scripts to get directory
paths without changing directories:
0 - Match found1 - No match found (used by shell integration)#!/bin/bash
# Switch to a git worktree using gump
worktree=$(gump query --all "$1" | while read dir; do
if git -C "$dir" rev-parse --is-inside-work-tree &>/dev/null; then
echo "$dir"
break
fi
done)
if [[ -n "$worktree" ]]; then
cd "$worktree"
else
echo "No git worktree found matching: $1"
fi | Feature | gump | zoxide | autojump | z.lua |
|---|---|---|---|---|
| No prefix | โ | โ | โ | โ |
| CWD matching | โ | โ | โ | โ |
| Multi-term | โ | โ | โ | โ |
| Fuzzy matching | โ (nucleo) | โ | โ | โ |
| Import | โ | โ | โ | โ |
| Atomic DB writes | โ | โ | โ | โ |
| Language | Rust | Rust | Python | Lua |
| Binary size | ~1.5MB | ~2MB | Script | Script |
Gump is built with a modular architecture in Rust, consisting of four main components:
gump/
โโโ src/
โ โโโ main.rs # Entry point and command dispatch
โ โโโ cli.rs # Clap command definitions
โ โโโ cmd/ # Subcommand implementations
โ โ โโโ add.rs # Add directory to database
โ โ โโโ query.rs # Fuzzy search database
โ โ โโโ remove.rs # Remove from database
โ โ โโโ list.rs # List all entries
โ โ โโโ clean.rs # Remove stale entries
โ โ โโโ init.rs # Generate shell integration
โ โ โโโ import.rs # Import from other tools
โ โ โโโ edit.rs # Edit database in $EDITOR
โ โโโ db/ # Database layer
โ โ โโโ entry.rs # DirEntry struct and frecency
โ โ โโโ store.rs # Database I/O and operations
โ โโโ matcher/ # Fuzzy matching layer
โ โโโ fuzzy.rs # Nucleo integration and scoring
โโโ Cargo.toml # Dependencies and build config [profile.release]
lto = true # Link-time optimization
codegen-units = 1 # Single codegen unit for better optimization
strip = true # Strip debug symbols Frecency is a ranking metric that combines frequency (how often you visit) with recency (how recently you visited). This ensures directories you use frequently AND recently rank higher than those visited often in the past but not recently.
frecency = raw_score ร time_multiplier
where:
raw_score = number of accesses (increments by 1 each visit)
time_multiplier = based on last_accessed timestamp Each directory has a raw score that increases by 1.0 on every access:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirEntry {
/// Raw access score (increments by 1 on each access)
pub score: f64,
/// Last time this directory was accessed
pub last_accessed: DateTime<Utc>,
/// Total number of times accessed
pub access_count: u64,
}
impl DirEntry {
pub fn record_access(&mut self) {
self.score += 1.0;
self.last_accessed = Utc::now();
self.access_count += 1;
}
} The frecency calculation applies time-based multipliers to favor recent directories:
| Option | Type | Default | Description |
|---|---|---|---|
Within 1 hour | - | ร4 | Recent directories get maximum boost |
Within 1 day | - | ร2 | Today's directories get moderate boost |
Within 1 week | - | ร0.5 | This week's directories get slight penalty |
Older | - | ร0.25 | Old directories get significant penalty |
pub fn frecency(&self) -> f64 {
let now = Utc::now();
let duration = now.signed_duration_since(self.last_accessed);
let hours = duration.num_hours();
let multiplier = if hours < 1 {
4.0 // Within 1 hour
} else if hours < 24 {
2.0 // Within 1 day
} else if hours < 24 * 7 {
0.5 // Within 1 week
} else {
0.25 // Older
};
self.score * multiplier
} | Directory | Raw Score | Last Accessed | Frecency |
|---|---|---|---|
| ~/projects | 50 | 30 min ago | 50 ร 4 = 200 |
| ~/.config | 100 | 2 days ago | 100 ร 0.5 = 50 |
| ~/old-project | 500 | 2 weeks ago | 500 ร 0.25 = 125 |
Despite ~/old-project having the highest raw score (500), ~/projects ranks highest due to the 4ร recency boost.
To prevent scores from growing indefinitely and to naturally decay unused entries, gump applies aging when the total score exceeds GUMP_MAXAGE (default 10000):
fn maybe_age(&mut self) {
let total: f64 = self.data.entries.values().map(|e| e.score).sum();
if total > self.max_age {
// Calculate reduction factor (target 90% of max)
let k = total / (self.max_age * 0.9);
// Divide all scores, remove entries below 1.0
self.data.entries.retain(|_, entry| {
entry.score /= k;
entry.score >= 1.0
});
}
} The aging process:
Gump uses the nucleo library for fuzzy matching - the same engine that powers the Helix editor's fuzzy finder. This provides fast, accurate matching with intelligent scoring.
Nucleo provides the core fuzzy matching capabilities:
use nucleo::{
pattern::{CaseMatching, Normalization, Pattern},
Matcher as NucleoMatcher, Utf32Str,
};
pub struct Matcher {
nucleo: NucleoMatcher,
}
impl Matcher {
pub fn new() -> Self {
Self {
nucleo: NucleoMatcher::new(nucleo::Config::DEFAULT),
}
}
pub fn score(&mut self, path: &Path, terms: &[String]) -> Option<u32> {
let path_str = path.to_string_lossy();
let mut haystack_buf = Vec::new();
let haystack = Utf32Str::new(&path_str, &mut haystack_buf);
let mut total_score: u32 = 0;
for term in terms {
let pattern = Pattern::new(
term,
CaseMatching::Ignore, // Case insensitive
Normalization::Smart, // Smart Unicode normalization
nucleo::pattern::AtomKind::Fuzzy,
);
match pattern.score(haystack, &mut self.nucleo) {
Some(score) => total_score = total_score.saturating_add(score),
None => return None, // All terms must match
}
}
Some(total_score)
}
} // Boost score if last term matches last path component
if let Some(last_term) = terms.last() {
if let Some(last_component) = path.file_name() {
// Check fuzzy match on last component
if pattern.score(comp_haystack, &mut self.nucleo).is_some() {
total_score = total_score.saturating_add(100);
// Extra boost for exact match
if last_comp_lower == last_term_lower {
total_score = total_score.saturating_add(50);
}
}
}
} | Query | Path | Fuzzy Score | Notes |
|---|---|---|---|
| myapp | /home/user/projects/myapp | ~250 | +100 last comp, +50 exact |
| myapp | /home/user/myapp/src | ~100 | No last comp match |
| mpp | /home/user/projects/myapp | ~150 | Fuzzy +100 last comp |
The final ranking combines fuzzy match quality with frecency. Critically, fuzzy score is the PRIMARY factor - frecency only adds a small bonus:
// Frecency adds up to 25% bonus for tiebreaking
frecency_bonus = 1.0 + (frecency / 160.0).min(0.25)
// Fuzzy score is the base, frecency is secondary
combined_score = fuzzy_score ร frecency_bonus This design ensures:
Gump stores its database in bincode format (fast binary serialization) with atomic writes for safety.
// On-disk format (bincode serialized)
struct DatabaseFile {
version: u32, // Schema version (currently 1)
entries: HashMap<PathBuf, DirEntry>,
last_cleanup: DateTime<Utc>,
}
// Each directory entry
struct DirEntry {
score: f64, // Raw access score
last_accessed: DateTime<Utc>, // UTC timestamp
access_count: u64, // Total accesses
} Schema versioning allows future migrations while maintaining backwards compatibility.
Database writes are atomic to prevent corruption from concurrent shell sessions or crashes:
pub fn save(&self) -> Result<(), DatabaseError> {
// Ensure parent directory exists
if let Some(parent) = self.path.parent() {
fs::create_dir_all(parent)?;
}
// Serialize to bytes
let bytes = bincode::serialize(&self.data)?;
// Write to temp file first
let temp_path = self.path.with_extension("bin.tmp");
{
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&temp_path)?;
file.write_all(&bytes)?;
file.sync_all()?; // Ensure data hits disk
}
// Atomic rename
fs::rename(&temp_path, &self.path)?;
Ok(())
} The write-to-temp-then-rename pattern ensures the database is never in a partially-written state, even if the process crashes mid-write.
Paths are canonicalized before storage to ensure consistency:
fn canonicalize_path<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf, DatabaseError> {
let path = path.as_ref();
// Handle "." specially
let path = if path == Path::new(".") {
std::env::current_dir()?
} else {
path.to_path_buf()
};
// Try to canonicalize (resolves symlinks)
match fs::canonicalize(&path) {
Ok(p) => Ok(p),
Err(_) => {
// If canonicalize fails, make it absolute
if path.is_absolute() {
Ok(path)
} else {
let cwd = std::env::current_dir()?;
Ok(cwd.join(path))
}
}
}
}
This ensures ~/projects,
/home/user/projects, and
./projects (from home) all resolve to the
same database entry.
Each shell uses its native hook mechanisms for tracking and command interception.
Bash's command_not_found_handle is called after all command resolution fails, making it perfect for no-prefix jumping.
Zsh's ZLE (Zsh Line Editor) allows intercepting input before execution via custom widgets, providing more control than Bash.
Fish's event system and key binding provide clean integration without the complexity of Bash/Zsh hook systems.
gump init <shell>Generate shell integration code. Output should be evaluated in your shell config.
| Option | Type | Default | Description |
|---|---|---|---|
--cmd <name> | string | g | Custom command name (e.g., j, z, cd) |
--hook <mode> | prompt|pwd | pwd | When to update the database |
--no-cmd | flag | false | Skip g/gi aliases, keep no-prefix jumping only |
gump query [terms...]Query the database for matching directories. Used internally by shell integration but useful for scripting.
| Option | Type | Default | Description |
|---|---|---|---|
<terms...> | string[] | - | Space-separated search terms for fuzzy matching |
--score, -s | flag | false | Show combined frecency + fuzzy scores |
--all, -a | flag | false | Show all matches instead of just the best |
--cwd | flag | false | Match against current directory contents only |
gump add [path]Add a directory to the database. Defaults to current directory if not specified.
gump remove <path>Remove a directory from the database.
gump listList all directories in the database.
| Option | Type | Default | Description |
|---|---|---|---|
--score, -s | flag | false | Show frecency scores alongside paths |
gump cleanRemove entries for directories that no longer exist AND are older than 90 days.
gump importImport history from zoxide, autojump, z, z.lua, zsh-z, and fasd.
gump editEdit the database in your editor. Exports to JSON, opens $EDITOR, then imports changes.
When using gump init, these aliases are created
(unless --no-cmd is specified):
| Alias | Description |
|---|---|
g [terms] | Jump to best match for terms |
g | Go home (cd ~) |
g - | Go back (cd -) |
g /path | Direct cd (no fuzzy matching) |
gi [terms] | Interactive selection with fzf |
| Code | Meaning |
|---|---|
0 | Success / match found |
1 | No match found / path not in database |
Directory names run as "command not found" instead of jumping.
Ensure you've added the init line to your shell config and restarted or sourced it. The init shell type must match your actual shell.
Gump jumps to an unexpected directory.
Remember: fuzzy score is primary, frecency is secondary. If another path has a better fuzzy match, it will win. Add more terms to be more specific.
New directories aren't being tracked.
Check if the directory is in GUMP_EXCLUDE. Try using --hook prompt
instead of --hook pwd to catch all navigation.
Local directories take precedence when you want the database match.
This is by design - local directories always win. Use the g
command or more specific terms to override.
Gump shows serialization errors when loading database.
Corruption is rare due to atomic writes, but can happen with disk errors. Delete the database file to start fresh.
gi command fails or shows nothing.
Interactive mode requires fzf to be installed and in PATH. Install it with your package manager.
Gump takes noticeable time to respond.
Large databases can slow down queries. Run clean regularly and lower GUMP_MAXAGE if you have too many entries.
"No databases found to import from"
Gump checks standard locations for each tool. If you have custom paths set via environment variables (Z_DATA, etc.), ensure they're exported.
If you're still experiencing issues:
gump --version to ensure you have the latest versiongump init bash