Jesse A. Green / notebook

The Fuck — Administrator & Developer Guide

Generated April 2026 from nvbn/thefuck at c7e7e1d

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

The complete source-derived reference for engineers, packagers, security reviewers, and teams shipping The Fuck at scale or extending it. The User Guide covers daily usage; this document covers architecture, internals, the rule plugin protocol, and the operational surface.

1. Architecture Overview

What it is

The Fuck (thefuck 3.32 by Vladimir Iakovlev, MIT license) is a Python CLI that reads the most recently failed shell command, runs it through an in-process pipeline of pluggable correction rules, and emits a corrected command on stdout. A small shell function — installed via eval $(thefuck --alias) — captures shell history, invokes the binary, and evals its output. The corrected command can also be written back into the shell history so the up-arrow recall is the fix rather than the typo.

There is no daemon, no server, no socket (with one optional exception covered below). Everything is single-process, single-shot, no persistent state beyond a small XDG cache.

High-level component diagram

                    ┌──────────────────────────────────────────────┐
                    │                INTERACTIVE SHELL                │
                    │  (Bash / Zsh / Fish / Tcsh / PowerShell)        │
                    │                                                  │
                    │     fuck()  ◄── shell function/alias             │
                    │       │                                          │
                    │       │ exports TF_HISTORY, TF_SHELL,            │
                    │       │ TF_SHELL_ALIASES, PYTHONIOENCODING       │
                    │       ▼                                          │
                    │     thefuck (Python binary)                      │
                    └──────────────────────────────────────────────┘
                                          │ stdout
                                          │  (a single corrected command string)
                                          ▼
                                       eval


                  ┌────────────────────────────────────────────────────┐
                  │                  thefuck.entrypoints.main            │
                  │   ─────────────────────────────────────────────────  │
                  │   Parser  →  branch on -v / -a / -l / command       │
                  │                                                      │
                  │     -a    →  alias.print_alias       (Section 4)     │
                  │     -l    →  shell_logger.shell_logger               │
                  │     cmd   →  fix_command.fix_command                 │
                  └────────────────────────────────────────────────────┘
                                          │
                                          ▼
   ┌─────────────────────────────────────────────────────────────────────────┐
   │                        thefuck.entrypoints.fix_command                    │
   │   ────────────────────────────────────────────────────────────────────── │
   │   1.  settings.init(args)                  ← merge file + env + flags    │
   │   2.  raw = _get_raw_command(args)         ← from arg, TF_HISTORY, fc    │
   │   3.  Command.from_raw_script(raw)         ← shell.from_shell + output   │
   │   4.  corrected = get_corrected_commands() ← rule pipeline               │
   │   5.  selected = select_command(corrected) ← arrow-key UI                │
   │   6.  selected.run(command)                ← side_effect + history       │
   └─────────────────────────────────────────────────────────────────────────┘
                                          │
              ┌───────────────────────────┼─────────────────────────────┐
              ▼                           ▼                             ▼
   ┌──────────────────┐    ┌──────────────────────────┐    ┌──────────────────────┐
   │ thefuck.shells    │    │  thefuck.output_readers   │    │  thefuck.corrector   │
   │ (.bash, .zsh,     │    │  (.rerun  ← default)      │    │   (rule loader,      │
   │  .fish, .tcsh,    │    │  (.read_log ← instant)    │    │    sort, dedup)      │
   │  .powershell,     │    │  (.shell_logger ← socket) │    │                      │
   │  .generic)        │    └──────────────────────────┘    └──────────────────────┘
   └──────────────────┘                  │                             │
                                          ▼                             ▼
                                 (the failing command)        thefuck.rules.*  (169 files)
                                  is re-executed and          + ~/.config/thefuck/rules/*
                                  output captured              + thefuck_contrib_*/rules/*

Note

Every box above is an actual Python module under thefuck/ with the matching path. There is no dynamic dispatch beyond stdlib importlib.util.spec_from_file_location for loading rule files. The "shell" object is selected once at startup by walking the parent process tree (psutil.Process(os.getpid()).parent()) until a known name is found.

Component inventory

Component Path Role
Argument parser thefuck/argument_parser.py Defines the public CLI surface, handles the THEFUCK_ARGUMENT_PLACEHOLDER trick that lets the alias pass arbitrary commands without conflicting with thefuck's own flags
Settings thefuck/conf.py The Settings(dict) class with attribute access. Loads defaults, then ~/.config/thefuck/settings.py, then env vars, then args
Constants thefuck/const.py DEFAULT_SETTINGS, ENV_TO_ATTR mapping, key codes, sentinel objects, log size limits, the USER_COMMAND_MARK (U+200B × 10) used by instant mode
Corrector thefuck/corrector.py Walks rule import paths, loads rules, sorts by priority, yields unique CorrectedCommands
Types thefuck/types.py Command, Rule, CorrectedCommand — the three load-bearing dataclasses
Exceptions thefuck/exceptions.py EmptyCommand, NoRuleMatched, ScriptNotInLog
Logging thefuck/logs.py colorama-based stderr formatting, debug timing context manager, "how to configure alias" prints
UI thefuck/ui.py read_actions() keypress loop, CommandSelector lazy iterator, select_command() — the up/down/enter prompt
Utilities thefuck/utils.py memoize, cache, which, get_close_matches, get_all_executables, the Cache class wrapping shelve
System thefuck/system/ Platform-split: unix.py and win32.py, plus __init__.py that picks one. Provides Path, init_output, get_key, open_command
Argument entry thefuck/entrypoints/main.py The setuptools console_scripts entry. Imports system.init_output first to set up colorama
Alias entry thefuck/entrypoints/alias.py Calls shell.app_alias() and prints
Fix entry thefuck/entrypoints/fix_command.py The pipeline orchestrator
Not-configured entry thefuck/entrypoints/not_configured.py The "how to configure" splash + auto-write-on-second-run logic. Maps to the fuck binary on Linux/macOS and thefuck_firstuse on Windows
Shell-logger entry thefuck/entrypoints/shell_logger.py The PTY recorder used by experimental instant mode
Shell adapters thefuck/shells/ Generic, Bash, Zsh, Fish, Tcsh, Powershell — each implements app_alias(), get_aliases(), from_shell(), to_shell(), how_to_configure(), _get_history_file_name()
Output readers thefuck/output_readers/ rerun.py (default, re-executes the script), read_log.py (instant mode, reads from THEFUCK_OUTPUT_LOG), shell_logger.py (Unix socket integration)
Rule package thefuck/rules/ 169 rule modules, one per correction pattern
Specific helpers thefuck/specific/ Per-tool helpers: apt_available, archlinux_env, brew_available/get_brew_path_prefix, dnf_available, git_support decorator, nix_available, npm_available/get_scripts, sudo_support decorator, yum_available

Technology stack

Layer Tool / library Purpose
Language Python ≥2.7 (3.5+ recommended) Single source-tree supports both via six shims
Process inspection psutil Walking parent process tree for shell detection; killing the rerun child cleanly on timeout
ANSI / Windows colors colorama (+ win_unicode_console on Win) Cross-platform colored stderr
Compatibility shims six, pathlib2 (Py2), decorator Decorator factories that work in 2/3, Path polyfill
Optional pyte Terminal emulator used by instant mode and the shell-logger reader to reconstruct line-wrapped output
Build setuptools, fastentrypoints A vendored fastentrypoints.py patches setuptools so console scripts skip the slow pkg_resources import
Tests pytest, mock, pytest-mock, pytest-docker-pexpect Unit + functional matrix that boots a shell in a Docker container

Data flow — what happens when you type fuck

  1. The alias function (a few lines of Bash/Zsh/Fish/Tcsh/PowerShell shipped by shell.app_alias()) saves $PYTHONIOENCODING, exports TF_PYTHONIOENCODING for restore, exports TF_SHELL, TF_ALIAS, TF_SHELL_ALIASES (Bash/Zsh from $(alias)), TF_HISTORY (Bash/Zsh from $(fc -ln -10); Fish from $history[1]; PowerShell from Get-History -Count 1), forces PYTHONIOENCODING=utf-8, then runs thefuck $ARG_PLACEHOLDER "$@" in command substitution and evals the result.
  2. thefuck/entrypoints/main.py initialises colorama via system.init_output() before importing anything else (because colorama.init patches stdout on Windows and other modules might already write to it). Then it constructs Parser, parses sys.argv, and dispatches to print_alias, fix_command, or shell_logger.
  3. fix_command (thefuck/entrypoints/fix_command.py) calls settings.init(args) — this:
    • creates ~/.config/thefuck/ and ~/.config/thefuck/rules/ if needed (legacy ~/.thefuck/ is honoured with a DeprecationWarning).
    • creates settings.py from const.SETTINGS_HEADER if missing.
    • imports settings.py via importlib.util and copies any keys from DEFAULT_SETTINGS.keys().
    • reads each THEFUCK_* env var listed in const.ENV_TO_ATTR and converts via _val_from_env (string → int/bool/list/dict).
    • applies args.yes, args.debug, args.repeat overrides.
  4. Raw command extraction (_get_raw_command):
    • if --force-command was passed (used by --repeat), use that string verbatim.
    • else if TF_HISTORY is in env (alias path), reverse the history, drop entries that are themselves the alias (using a 0.5 SequenceMatcher cutoff vs TF_ALIAS), and return the first non-alias line.
    • else use the positional command args verbatim.
  5. Command.from_raw_script joins the raw parts to a script, runs shell.from_shell() to expand any shell aliases (Bash/Zsh: parse TF_SHELL_ALIASES; Fish: invoke fish -ic alias and fish -ic functions; Tcsh: invoke tcsh -ic alias), and calls output_readers.get_output().
  6. Output reading (thefuck/output_readers/__init__.py) picks one path:
    • if SHELL_LOGGER_SOCKET env var points at an existing socket → ask it for the last 5 commands' output.
    • else if settings.instant_mode is True → read from THEFUCK_OUTPUT_LOG mmap, group by USER_COMMAND_MARK/PS1, and feed through pyte.Screen to reconstruct the terminal display.
    • else → Popen(expanded, shell=True, ..., env=os.environ + settings.env) and wait wait_command (or wait_slow_command if the binary is in slow_commands). On timeout, kill the entire process tree (children first via psutil.Process(pid).children(recursive=True)).
  7. Rule pipeline (thefuck.corrector.get_corrected_commands):
    • get_rules_import_paths() yields thefuck/rules/, ~/.config/thefuck/rules/, and every thefuck_contrib_*/rules/ directory found anywhere on sys.path.
    • get_rules() globs *.py (skipping __init__.py), passes each to Rule.from_path which loads it via importlib.util.spec_from_file_location (or legacy imp.load_source on Python <3.5), and reads match, get_new_command, enabled_by_default, side_effect, priority, requires_output off the module.
    • rules are filtered by is_enabled (name in settings.rules OR (enabled_by_default AND ALL_ENABLED in settings.rules)) AND name not in settings.exclude_rules.
    • rules are sorted ascending by priority (default DEFAULT_PRIORITY = 1000, overridden per-rule via priority = N and per-user via settings.priority['rule_name'] = N).
    • for each rule, is_match(command) is called inside a debug_time context. If the rule throws, it logs the traceback and continues (no rule failure ever crashes the pipeline).
    • matching rules' get_new_command results are wrapped in CorrectedCommand (one per result string; if the rule returns a list, each gets its own CorrectedCommand with priority scaled by (n+1) * rule.priority).
    • organize_commands deduplicates (by script and side_effect) and sorts again.
  8. Selector UI (thefuck/ui.py):
    • CommandSelector is a lazy iterator — it pulls the first command eagerly to detect "no fucks given", but the rest are realised only when the user presses ↑/↓.
    • read_actions() blocks on system.get_key() (raw termios on Unix, msvcrt.getwch on Windows) and yields ACTION_* constants.
    • if require_confirmation is False, the first command is auto-selected.
  9. Run (CorrectedCommand.run):
    • if a side_effect is defined, call it with (old_cmd, new_script) BEFORE printing.
    • call shell.put_to_history(script) (Fish writes a literal entry; Bash/Zsh/Tcsh defer to the shell function's history -s / print -s line).
    • if settings.repeat is True, wrap the script in shell.or_(script, "fuck --repeat --force-command <quoted-original>") so a failure re-runs the loop.
    • finally, print to stdout. The shell alias evals that.

Important

The architectural invariant: The Fuck never executes the corrected command itself. It writes to stdout; the shell function's eval is what runs it. This means corrections execute in the user's interactive shell — same cwd, same env, same shell options — and the rerun used for capturing output is a different subprocess that does not see the side effects.


2. System Requirements

Runtime

Layer Minimum Recommended Notes
Python 2.7 or 3.5 3.10–3.11 Python 2 is deprecated; 3.12+ has known issues with imp and distutils (issues #1434/#1444). 3.13 issues with imp are tracked in #1527.
pip 6.0 latest Hard check in setup.py:11.
psutil any latest Used for parent-process walking and process kill.
colorama any latest Pure Python; works everywhere.
six any latest Py2/Py3 compatibility shim.
pyte optional latest Required for instant mode and shell-logger reader.
decorator any latest Used to compose @for_app, @sudo_support, @git_support, @cache, @memoize, @eager.
pathlib2 optional — Auto-installed for Python <3.4 only.
backports.shutil_get_terminal_size optional — Auto-installed for Python <3.3 only.
win_unicode_console optional — Auto-installed on win32 only.

Shells

Shell Min version tested Alias source History capture
Bash 3.x BASH_VERSION via bash -c 'echo $BASH_VERSION' fc -ln -10
Zsh 4+ ZSH_VERSION via zsh -c 'echo $ZSH_VERSION' fc -ln -10
Fish 2+ fish --version $history[1] plus ~/.config/fish/fish_history
Tcsh / Csh 6+ tcsh --version history -h 2 | head -n 1
PowerShell 5+ / pwsh $PSVersionTable.PSVersion or pwsh --version Get-History -Count 1
Generic — — Falls back to alias-on-tty pattern

OS / platform

Platform Status
Linux (any distro) Fully supported.
macOS Fully supported.
FreeBSD / OpenBSD Supported via pkg/ports. --shell-logger works.
Windows Supported. Drops --shell-logger and instant mode. Ships scripts/fuck.bat and scripts/fuck.ps1.
WSL Works; you may need TF_SHELL=bash if process-tree detection lands on init.
Termux (Android) Works; PATH adjustments may be needed (issue #1369).
Docker / CI Works in non-interactive mode with THEFUCK_REQUIRE_CONFIRMATION=false.

Optional system tools

Tool Used by Notes
script(1) instant mode bsdutils (Debian/Ubuntu), util-linux (Arch), built-in (macOS).
lsof port_already_in_use rule Disabled if missing.
pkgfile pacman, pacman_not_found rules Arch Linux only.
apt-get, dnf, yum, brew, nix corresponding rules Each rule self-disables via <tool>_available if the binary is not on PATH.

3. Configuration Reference

Configuration files

File Format Default location Created by
settings.py Python source ${XDG_CONFIG_HOME:-~/.config}/thefuck/settings.py Auto-generated on first run from const.SETTINGS_HEADER
User rules Python source ${XDG_CONFIG_HOME:-~/.config}/thefuck/rules/*.py Manually created — directory pre-created on first run
Shelve cache shelve (dbm) ${XDG_CACHE_HOME:-~/.cache}/thefuck Auto-created by Cache._setup_db
Legacy config Python source ~/.thefuck/settings.py and ~/.thefuck/rules/ Used only if it pre-exists; printed deprecation warning

The complete settings dictionary

Defined in thefuck/const.py. Every key has a default; every key is overridable via env var listed in ENV_TO_ATTR.

Setting Type Default Purpose
rules list[str] [ALL_ENABLED] Rule whitelist. Use the sentinel ALL_ENABLED (env: literal string DEFAULT_RULES) to mean "every rule whose enabled_by_default is True".
exclude_rules list[str] [] Rules to skip (subtractive — runs even with ALL_ENABLED).
wait_command int (s) 3 Subprocess timeout for the rerun.
wait_slow_command int (s) 15 Subprocess timeout when the rerun's first arg is in slow_commands.
slow_commands list[str] ['lein', 'react-native', 'gradle', './gradlew', 'vagrant'] Binaries that get the longer rerun timeout.
require_confirmation bool True If False, auto-runs the first suggestion.
no_colors bool False Disables colorama escape sequences.
debug bool False Emits [DEBUG] lines and debug_time durations to stderr.
priority dict[str,int] {} Per-rule priority override. Lower value = earlier.
history_limit int | None None Caps how many lines of history are read by _get_history_lines.
alter_history bool True Push the corrected command into shell history.
repeat bool False Wrap the corrected command so a failure re-runs fuck.
instant_mode bool False Use the shell-logger output reader instead of the rerun.
num_close_matches int 3 How many suggestions to ask difflib.get_close_matches for.
env dict[str,str] {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'} Env vars added to the rerun subprocess.
excluded_search_path_prefixes list[str] [] PATH directories to skip when listing executables (avoids slow NFS mounts).
fixlinecmd str '{editor} {file} +{line}' Used by fix_file rule. Loaded only when that rule runs.
fixcolcmd str | None None Optional fix_file template that includes column position.
user_dir Path ~/.config/thefuck Resolved at init by _get_user_dir_path. Honours XDG_CONFIG_HOME.

Environment variable map

The exact mapping in const.ENV_TO_ATTR:

ENV_TO_ATTR = {
    'THEFUCK_RULES':                          'rules',
    'THEFUCK_EXCLUDE_RULES':                  'exclude_rules',
    'THEFUCK_WAIT_COMMAND':                   'wait_command',
    'THEFUCK_REQUIRE_CONFIRMATION':           'require_confirmation',
    'THEFUCK_NO_COLORS':                      'no_colors',
    'THEFUCK_DEBUG':                          'debug',
    'THEFUCK_PRIORITY':                       'priority',
    'THEFUCK_HISTORY_LIMIT':                  'history_limit',
    'THEFUCK_ALTER_HISTORY':                  'alter_history',
    'THEFUCK_WAIT_SLOW_COMMAND':              'wait_slow_command',
    'THEFUCK_SLOW_COMMANDS':                  'slow_commands',
    'THEFUCK_REPEAT':                         'repeat',
    'THEFUCK_INSTANT_MODE':                   'instant_mode',
    'THEFUCK_NUM_CLOSE_MATCHES':              'num_close_matches',
    'THEFUCK_EXCLUDED_SEARCH_PATH_PREFIXES':  'excluded_search_path_prefixes',
}

Settings._val_from_env parses values:

Setting Parse rule
rules, exclude_rules Split on :. The string DEFAULT_RULES expands to [ALL_ENABLED].
priority Split on :, then split each on =. Drops malformed pairs silently.
wait_command, history_limit, wait_slow_command, num_close_matches int(val)
require_confirmation, no_colors, debug, alter_history, instant_mode val.lower() == 'true'
slow_commands, excluded_search_path_prefixes Split on :.
Anything else passed through as string.

Important

Changing settings.py requires no restart — settings are read fresh on every invocation. Changing the alias (re-running eval $(thefuck --alias)) does require sourcing your shell rc again because the alias is a function in your current shell.

CLI flag reference (full)

Flag dest action Default Mutex group
-v, --version version store_true False
-a, --alias [NAME] alias nargs=?, const=get_alias() None
-l PATH, --shell-logger PATH shell_logger store None
--enable-experimental-instant-mode enable_experimental_instant_mode store_true False
-h, --help help store_true False
-d, --debug debug store_true False
--force-command CMD force_command store (suppressed in --help) None
command command nargs='*' []
-y, --yes, --yeah, --hard yes store_true False yes/repeat
-r, --repeat repeat store_true False yes/repeat

The argument prep step in Parser._prepare_arguments rewrites argv so The Fuck's flags are parsed first, then -- separates them from the user's command. The placeholder THEFUCK_ARGUMENT_PLACEHOLDER is used inside the alias to mark the boundary.

Annotated walkthroughs

Workplace-friendly setup

# ~/.config/thefuck/settings.py
rules = ['DEFAULT_RULES']  # equivalent to ALL_ENABLED
exclude_rules = ['rm_root']  # belt-and-braces — already off by default

# Auto-confirm only when run with -y; keep prompts otherwise
require_confirmation = True

# Don't write fixed commands into shell history
alter_history = False

# Enterprise NFS mount that we don't want to scan for executables
excluded_search_path_prefixes = ['/mnt/corp-share', '/var/empty']

# Stricter close-match cutoff via priority
priority = {
    'sudo': 5000,        # try sudo only after most other rules
    'history': 9999,     # last resort
}

Performance-tuned setup for a slow-shell environment

# Limit imports to a small whitelist for snappy first-call
rules = [
    'cd_correction', 'cd_mkdir', 'cd_parent',
    'git_push', 'git_pull', 'git_add', 'git_branch_delete',
    'mkdir_p', 'sudo', 'no_command',
]

# Don't memoize — for a stateless container
# (memoize.disabled is set elsewhere; cache.disabled via env)

# Bigger rerun budget for builds
wait_slow_command = 60
slow_commands = ['gradle', './gradlew', 'mvn', 'lein', 'react-native', 'vagrant', 'docker-compose', 'sbt']
export THEFUCK_NUM_CLOSE_MATCHES=5

Instant-mode setup

# In ~/.zshrc, MUST be the last line
eval "$(thefuck --alias --enable-experimental-instant-mode)"
# ~/.config/thefuck/settings.py
instant_mode = True

instant_mode = True makes the output reader prefer read_log over rerun. The alias adds:

export THEFUCK_INSTANT_MODE=True
export THEFUCK_OUTPUT_LOG=/tmp/thefuck-script-log-<uuid>
thefuck --shell-logger $THEFUCK_OUTPUT_LOG
rm $THEFUCK_OUTPUT_LOG
exit

Effectively the alias replaces the user's shell with a script(1)-equivalent recorder. When the user invokes fuck, the engine reads from the mmap'd log instead of re-running.

Warning

Instant mode permanently alters your PS1 for the lifetime of the shell. If you have a prompt theme that re-sets PS1 on every command, instant mode silently breaks (you'll see [WARN] PS1 doesn't contain user command mark on first invocation).


4. Deployment & Scaling

Distribution channels

Channel Recipe
PyPI pip install thefuck (built from setup.py via python setup.py sdist bdist_wheel && twine upload dist/* — see release.py).
Homebrew brew install thefuck — formula tracks the GitHub release tag.
Snap snapcraft.yaml defines name: thefuck, classic confinement, exposes both thefuck and fuck apps with PYTHONIOENCODING=utf-8.
Debian / Ubuntu Distro packagers maintain thefuck in the universe repo.
Fedora / Arch / FreeBSD Distro packages exist; track upstream tags.
Windows pip install ships scripts/fuck.bat and scripts/fuck.ps1. The Windows entry-points map thefuck and thefuck_firstuse (NOT fuck, because fuck.exe would clash).

Building from source

git clone https://github.com/nvbn/thefuck.git
cd thefuck
pip install -r requirements.txt
pip install .
pytest

requirements.txt includes the dev-only deps (flake8, pytest, mock, pytest-mock, pypandoc, pytest-benchmark, pytest-docker-pexpect, twine, wheel, setuptools, pexpect).

Release process

release.py:

  1. Reads setup.py, finds VERSION = 'X.Y', increments minor.
  2. Writes back.
  3. git pull, git commit -am "Bump to X.Y+1", git tag X.Y+1, git push, git push --tags.
  4. rm -rf dist/*, python setup.py sdist bdist_wheel, twine upload dist/* — all with CONVERT_README=true so pypandoc converts the README from md to rst for PyPI.

Note

Setting CONVERT_README=true makes setup.py import pypandoc and convert README.md to RST for the long_description field. Without it, long_description is empty — fine for installs from a git checkout, problematic for PyPI uploads.

Scaling considerations

The Fuck is per-user, per-shell, single-shot. There is nothing to scale at the process level. Multi-user concerns:

  • Shared dotfiles repo: Many teams check in their shell rc plus a ~/.config/thefuck/ directory. The Fuck handles missing files gracefully; symlinks work fine.
  • Container images: Pre-install via pip install thefuck and pre-write settings.py with require_confirmation=False. This gives one-shot CLI corrections in CI.
  • Centralised rule packs: Publish a thefuck_contrib_<team> package on a private PyPI. The corrector auto-discovers any thefuck_contrib_* directory anywhere on sys.path and globs its rules/. Versioning the rule pack independently from The Fuck is a clean separation.

Multi-shell deployments

A single user may use multiple shells. Each shell needs its own alias line:

# In ~/.bashrc
[[ -n "$BASH_VERSION" ]] && eval "$(thefuck --alias)"

# In ~/.zshrc
eval $(thefuck --alias)

# In ~/.config/fish/config.fish
thefuck --alias | source

The Fuck detects the active shell at runtime via psutil, so the same Python install handles them all.


5. Operations

Health checks

There is no health endpoint. The closest equivalents:

Check Command Expected output
Binary present which thefuck && which fuck Two paths under ~/.local/bin (or equivalent)
Version thefuck --version The Fuck X.Y using Python A.B.C and {shell info}
Alias resolves thefuck --alias Shell function source on stdout
Settings load fuck -d 2>&1 | grep "Run with settings" Pretty-printed Settings dict
Rule import succeeds fuck -d 2>&1 | grep "Importing rule" One log line per rule

Tip

For a deployment-time smoke test:

THEFUCK_REQUIRE_CONFIRMATION=false thefuck git brnch 2>/dev/null \
  | grep -q "git branch" && echo OK || echo FAIL

This bypasses the alias and runs The Fuck against a known input.

Logging

  • All non-stdout output is stderr. Wrappers that capture stderr (CI logs, journald, etc.) will see the colored prompt and warnings.
  • --debug (or THEFUCK_DEBUG=true or debug=True) emits:
    • Run with settings: {...} (pretty-printed every-key dump)
    • Importing rule X took: H:MM:SS.dddddd (per rule)
    • Trying rule X took: ... (per match attempt)
    • Call: <script>; with env: {...}; is slow: True/False took: ... (per rerun)
    • Read output from log took: ... (instant mode)
    • Received output: <captured>
    • PYTHONIOENCODING: <value> after every successful run
    • Corrected commands: <pretty list> from the corrector
  • Rule failures log a full traceback to stderr but never crash the pipeline.
  • colorama.Style.RESET_ALL is always emitted between alerts to keep terminal state sane.

Note

The Fuck writes nothing to disk except (a) the auto-generated settings.py, (b) the shelve cache, (c) the last_not_configured_run_<user> JSON file in tempdir, and (d) the instant-mode thefuck-script-log-<uuid> file. All four are removable at any time.

Cache management

Cache.__init__ lazy-opens the shelve. Cache keys are SHA-stable: pickle.dumps((fn.__module__, repr(fn).split('at')[0], depends_on, args, kwargs)). The etag is the dot-joined os.path.getmtime of every depends_on path — so when /usr/local/bin/brew is upgraded, every @cache(which('brew'))(...) entry invalidates automatically.

Manually clear:

rm -rf ${XDG_CACHE_HOME:-~/.cache}/thefuck*

The cache is auto-deleted and re-created on a dbm.error (which fires when you switch Python versions and the dbm format mismatches — issue surfaced via the warning Removing possibly out-dated cache).

Monitoring

There is nothing to monitor — The Fuck does not run as a service. You can monitor the user's experience via shell history pattern matching ("how often does fuck immediately follow a failed command?") if you have shell history aggregation, but there is no built-in telemetry.

Backup / restore

No state to back up. The two user-mutable files are:

Path Restore procedure
~/.config/thefuck/settings.py Delete and let it regenerate; or restore from your dotfiles repo.
~/.config/thefuck/rules/*.py Restore from your dotfiles repo.

The shelve cache and tempfiles are safe to delete at any time.


6. Performance

Startup cost

Phase Approximate cost
Python interpreter start (no entry-point optimisation) 50–250 ms
Python interpreter start (with fastentrypoints.py) 30–100 ms
colorama.init + system.init_output <5 ms
Settings.init: read settings.py, env, args 5–20 ms
Shell detection (psutil.Process(getpid()).parent().name()) 5–20 ms
First call: rule package imports (169 rules + user + contrib) 200–500 ms
First call: rerun subprocess (default 3-second window) 0–3000 ms
First call: rule pipeline (is_match × N rules) 50–300 ms
Total typical first call 0.5 – 4 seconds
Subsequent calls (within shell lifetime; memoize hot) 0.3 – 2 seconds

fastentrypoints.py

Vendored from github.com/ninjaaron/fast-entry_points. It monkey-patches setuptools.command.easy_install.ScriptWriter.get_args so console scripts skip pkg_resources import (which is the dominant cost in stock setuptools entry-points). On large sys.path setups (Conda, virtualenvs with many extras), this can shave 100–300 ms per invocation.

Memoization

thefuck.utils.memoize:

def memoize(fn):
    memo = {}
    @wraps(fn)
    def wrapper(*args, **kwargs):
        if not memoize.disabled:
            key = pickle.dumps((args, kwargs))
            if key not in memo:
                memo[key] = fn(*args, **kwargs)
            value = memo[key]
        else:
            value = fn(*args, **kwargs)
        return value
    return wrapper

memoize.disabled = False

A process-lifetime LRU. Used on which, is_app, Bash.get_aliases, Tcsh.get_aliases, Zsh.get_aliases, _get_destination (in path_from_history), _get_executable (in missing_space_before_subcommand), and a dozen other expensive functions. Disable globally with monkeypatch.setattr('thefuck.utils.memoize.disabled', True) (the test suite does this in conftest).

Persistent cache

thefuck.utils.cache(*depends_on) and the underlying Cache class. Used by:

  • gem_unknown_command._get_all_commands (@cache(which('gem'))) — caches gem help commands output keyed by the gem binary's mtime.
  • docker_not_command.get_docker_commands (@cache(which('docker'))).
  • go_unknown_command.get_golang_commands.
  • grunt_task_not_found._get_all_tasks (@cache('Gruntfile.js')).
  • gulp_not_task.get_gulp_tasks (@cache('gulpfile.js')).
  • react_native_command_unrecognized._get_commands (@cache('package.json')).
  • yarn_command_not_found._get_all_tasks (@cache(which('yarn'))).
  • omnienv_no_such_command.get_app_commands per-binary.
  • Fish: _get_functions (@cache('~/.config/fish/config.fish', '~/.config/fish/functions')) and _get_aliases.

The cache is only consulted when a rule actually needs the data, so cold-start cost is unaffected.

Slow rerun pitfalls

The engine runs the failed command for up to wait_command seconds (default 3) to capture output. If the failure was a long-running command that crashed near the end, the rerun could complete the long part again. Mitigations:

  • Add the binary to slow_commands so it gets wait_slow_command (15s) instead.
  • Use instant mode (Bash/Zsh + Linux/macOS) to read from a recorded log instead.
  • Provide custom rules with requires_output = False so they skip the rerun entirely.

Tip

For a build tool like gradle, set requires_output = False on a custom rule and key off command.script alone. This avoids the 15-second penalty for tools you correct often.

Rule-import time

Each rule is imp.load_source'd (or importlib.util.spec_from_file_location'd on Py3) on every invocation. For 169 rules, this is the largest single cost. Mitigations:

  • Whitelist with rules = [...] to skip imports of irrelevant rules.
  • Exclude high-cost rules (e.g., pacman, pacman_not_found import archlinux.get_pkgfile which can shell out at module load time on non-Arch systems).
  • excluded_search_path_prefixes is honoured by get_all_executables — listing a slow NFS mount there saves time per invocation, not per rule.

7. Security

Threat model

The Fuck has elevated trust by design: it generates shell commands that the user's interactive shell evals. An attacker who can poison the inputs (the failing command, its output, or the rule files) can execute arbitrary code in the user's shell.

Attack surfaces

Surface Risk Mitigation
Untrusted rule files A malicious ~/.config/thefuck/rules/foo.py can run any code at import time and at match() time. Treat rule files as trusted input. Do not pip install thefuck_contrib_* from random sources. Audit before installing.
Compromised cache The shelve cache stores pickled values. If an attacker writes to ${XDG_CACHE_HOME}/thefuck, they can inject arbitrary objects on next read. The shelve file is 0600 mode by default; do not relax it. Delete the cache after any system compromise.
Compromised settings.py An attacker who writes to ~/.config/thefuck/settings.py runs Python at every fuck invocation. Same as above — ~/.config/thefuck/ should be 0700.
Output injection via remote command If a rule keys off a substring of the failing command's stderr, an attacker controlling that stderr (e.g., via SSH MOTD, captured Docker output) could craft a string that matches a destructive rule. Most rules check fixed strings; the few that parse user-controlled output (apt_get, apt_invalid_operation) only generate package names, not arbitrary scripts. The repeat = True setting amplifies the risk by wrapping the suggestion in a shell call back into fuck.
Side-effect rules dirty_untar deletes files in CWD; dirty_unzip likewise; ssh_known_hosts deletes known_hosts lines. All side-effect rules are filtered: dirty_untar only removes paths starting with os.getcwd() (so .. paths are preserved); ssh_known_hosts only edits the file path emitted by ssh's own warning. The (+side effect) marker in the prompt is a deliberate UX warning — keep require_confirmation=True for these.
sudo rule pattern matching The sudo rule's pattern list includes generic strings like "permission denied" that can appear in unrelated output. A doc string or a captured log could match. The rule never runs sudo itself; it just suggests sudo $command. The user must confirm. If the user disables confirmation, they have accepted the implicit risk.
rm_root rule Disabled by default — would suggest rm / --no-preserve-root. Keep it disabled. If you opt in, set priority['rm_root'] = 9999 so it never fires before more specific rules.
git_push_force rule Disabled by default — suggests --force-with-lease. Less destructive than --force but still risky on shared branches. Keep disabled in shared CI.
Instant-mode shell logger Records every command and its output to /tmp/thefuck-script-log-<uuid> (mode 0600). The log is mmapped at 1 MiB, with a 10 KiB high-water mark for cleanup. Anyone with read access to /tmp and the user's UID can see the log. Disable instant mode on shared boxes.
fix_file rule Invokes $EDITOR on a file path extracted from output. The rule requires the file to actually exist (os.path.isfile(m.group('file'))), so an attacker would have to also write the named file. Risk is mostly UX: opening a file you didn't expect.
Argument placeholder The THEFUCK_ARGUMENT_PLACEHOLDER = 'THEFUCK_ARGUMENT_PLACEHOLDER' constant is reserved. A user with a literal THEFUCK_ARGUMENT_PLACEHOLDER in their command would get scrambled args. The string is unique enough that this never happens in practice.

Warning

Never let ~/.config/thefuck/settings.py or any rule file be writable by another user. The Fuck imports them with full Python privileges. On multi-user systems, set chmod 0700 ~/.config/thefuck && chmod 0600 ~/.config/thefuck/settings.py.

Caution

Disabling require_confirmation (or always passing -y) gives The Fuck the authority to run any rule's output without your review. Combine that with a contrib rule pack you didn't audit and you have a privilege-escalation primitive. Do not run with require_confirmation=False in environments that trust your interactive shell more than your install path.

Network behaviour

The Fuck makes no outbound network calls itself. Rules that do call external commands (apt-get, brew, pkgfile, gem, docker, gradle --tasks, npm run-script, etc.) inherit the user's network access. There is no telemetry, no version-check, no analytics.

Secrets

The Fuck does not handle secrets. It does, however, capture command output via the rerun, which may include credentials echoed by the failing command. That captured output is held in memory only; instant mode persists it to the log file under /tmp (1 MiB rolling buffer, mode 0600).

Warning

If a command echoes a token or password to stderr and you use instant mode, that token is on disk under /tmp/thefuck-script-log-<uuid> until the shell exits. Disable instant mode on shared hosts or on hosts where /tmp is not user-private.

Authentication / authorisation

There is none. The Fuck operates as the invoking user and respects whatever permissions they have.

Code-signing / supply chain

Releases are uploaded to PyPI via twine upload (see release.py). PyPI checks PGP signatures if present but does not require them; the package on PyPI is the source of truth. Snap packages are confined classic (no sandboxing). Distro packages re-build from the upstream tag.

To verify integrity:

pip download thefuck --no-deps
pip hash thefuck-3.32.tar.gz
# Compare against the hash recorded by the GitHub release tag.

Reporting vulnerabilities

The project's issue tracker is the public channel. There is no SECURITY.md. For a sensitive disclosure, contact the author at [email removed].


8. Developer Internals

Repository structure

thefuck/
├── __init__.py             # empty
├── argument_parser.py      # Parser class, ARGUMENT_PLACEHOLDER trick
├── conf.py                 # Settings(dict) with attribute access
├── const.py                # DEFAULT_SETTINGS, ENV_TO_ATTR, sentinels
├── corrector.py            # get_rules, organize_commands, get_corrected_commands
├── exceptions.py           # EmptyCommand, NoRuleMatched, ScriptNotInLog
├── logs.py                 # color, warn, exception, debug_time, etc.
├── types.py                # Command, Rule, CorrectedCommand
├── ui.py                   # CommandSelector, select_command, read_actions
├── utils.py                # memoize, cache, which, get_alias, Cache class
│
├── entrypoints/
│   ├── alias.py            # print_alias
│   ├── fix_command.py      # the orchestrator
│   ├── main.py             # console_scripts entry
│   ├── not_configured.py   # the splash + auto-write-on-second-run
│   └── shell_logger.py     # PTY recorder for instant mode
│
├── output_readers/
│   ├── __init__.py         # get_output dispatcher
│   ├── rerun.py            # default — Popen + wait + kill
│   ├── read_log.py         # instant mode — mmap THEFUCK_OUTPUT_LOG
│   └── shell_logger.py     # external integration via Unix socket
│
├── rules/                  # 169 rule files (1 per correction pattern)
│   ├── __init__.py         # empty
│   ├── adb_unknown_command.py
│   ├── ag_literal.py
│   ├── apt_get.py
│   ├── ...
│   └── yum_invalid_operation.py
│
├── shells/                 # shell adapters
│   ├── __init__.py         # picks one via TF_SHELL or process tree
│   ├── bash.py             # Bash subclass of Generic
│   ├── fish.py             # Fish subclass of Generic
│   ├── generic.py          # base class — shell-agnostic ops
│   ├── powershell.py       # Powershell subclass of Generic
│   ├── tcsh.py             # Tcsh subclass of Generic
│   └── zsh.py              # Zsh subclass of Generic
│
├── specific/               # tool-specific helpers + decorators
│   ├── apt.py              # apt_available
│   ├── archlinux.py        # get_pkgfile, archlinux_env
│   ├── brew.py             # brew_available, get_brew_path_prefix
│   ├── dnf.py              # dnf_available
│   ├── git.py              # @git_support decorator
│   ├── nix.py              # nix_available
│   ├── npm.py              # npm_available, get_scripts
│   ├── sudo.py             # @sudo_support decorator
│   └── yum.py              # yum_available
│
└── system/
    ├── __init__.py         # imports unix or win32
    ├── unix.py             # tty/termios get_key, init_output, open_command
    └── win32.py            # msvcrt.getwch, win_unicode_console init_output

The three core types

Command (thefuck/types.py:12)

class Command(object):
    def __init__(self, script, output): ...
    @property
    def stdout(self): ...   # deprecated alias for output
    @property
    def stderr(self): ...   # deprecated alias for output
    @property
    def script_parts(self): ...   # lazy shell.split_command(script)
    def update(self, **kwargs): ...   # return new Command with overrides
    @classmethod
    def from_raw_script(cls, raw_script): ...

Command is the rule's view of the failing invocation. Two fields:

  • script: the canonicalised command string (after shell.from_shell alias expansion).
  • output: the captured stdout+stderr from the rerun (or the shell-logger reader).

script_parts is the shlex'd list, lazily computed because shlex.split is not free and many rules don't need it. The @property falls back to self.script.split(' ') if shlex fails (e.g., unbalanced quotes).

Rule (thefuck/types.py:85)

class Rule(object):
    def __init__(self, name, match, get_new_command,
                 enabled_by_default, side_effect,
                 priority, requires_output): ...

    @classmethod
    def from_path(cls, path): ...

    @property
    def is_enabled(self): ...

    def is_match(self, command): ...
    def get_corrected_commands(self, command): ...

from_path is the load-bearing classmethod:

@classmethod
def from_path(cls, path):
    name = path.name[:-3]   # strip .py
    if name in settings.exclude_rules:
        return None
    rule_module = load_source(name, str(path))   # importlib.util or imp
    priority = getattr(rule_module, 'priority', DEFAULT_PRIORITY)
    return cls(name,
               rule_module.match,
               rule_module.get_new_command,
               getattr(rule_module, 'enabled_by_default', True),
               getattr(rule_module, 'side_effect', None),
               settings.priority.get(name, priority),
               getattr(rule_module, 'requires_output', True))

So a rule file can declare any of these module-level names:

Name Default Meaning
match required (Command) -> bool. Called for every command after rule loading.
get_new_command required `(Command) -> str
enabled_by_default True Whether ALL_ENABLED includes this rule.
side_effect None (Command, str) -> None. Called BEFORE the new command is printed.
priority 1000 Lower = earlier. Per-user settings.priority overrides.
requires_output True If False, the rule runs even when there is no captured output (e.g., git_hook_bypass, dirty_unzip).

is_match runs the rule's match() inside a debug_time context, catches every exception, and logs Rule X failed: with traceback. Rule errors never propagate.

get_corrected_commands calls match.get_new_command(command) and yields one CorrectedCommand per result. If the result is a string, you get one. If a list, you get N (their priorities scaled by (n+1) * rule.priority so the first result wins ties).

CorrectedCommand (thefuck/types.py:201)

class CorrectedCommand(object):
    def __init__(self, script, side_effect, priority): ...
    def __eq__(self, other): ...   # ignores priority
    def __hash__(self): ...        # by (script, side_effect)
    def run(self, old_cmd): ...

Equality and hashing ignore priority so organize_commands can deduplicate identical scripts from different rules.

run:

  1. Calls side_effect(old_cmd, self.script) if non-None.
  2. Calls shell.put_to_history(self.script) if settings.alter_history.
  3. Logs PYTHONIOENCODING: <value> (debug).
  4. Writes the script to stdout. If settings.repeat, the script is wrapped in script || fuck --repeat --force-command "<quoted-script>".

The decorators

Authoring rules is mostly about composing decorators from thefuck.utils and thefuck.specific. The key ones:

Decorator Source Behaviour
@for_app('app1', 'app2', at_least=N) utils.py:1128 Wraps a match so it returns False unless command.script_parts[0] is one of the named apps and there are at least N+1 parts.
@sudo_support specific/sudo.py:7 If script starts with sudo , strip it, call inner, prepend sudo to result.
@git_support specific/git.py:7 Only call inner if is_app(command, 'git', 'hub'). Then parse trace: alias expansion: ALIAS => EXPANSION from output and rewrite command.script so rules see the resolved command.
@memoize utils.py:53 Process-lifetime LRU. Disabled by memoize.disabled = True.
@cache(*paths_or_binaries) utils.py:1211 Persistent shelve cache keyed by mtime of given paths. Disabled by cache.disabled = True.
@eager utils.py:1086 Wraps a generator so it's realised to a list before returning.
@default_settings({key: value, ...}) utils.py:1013 Adds default values to settings before calling inner. Lazy — applied only when the rule actually fires. Used by fix_file to inject fixlinecmd and fixcolcmd.

Authoring a custom rule — full template

# ~/.config/thefuck/rules/my_company_deploy.py
"""
Match: `deploy` command failing because the user forgot to source the
shared env. Suggest sourcing it and re-running.

Author: Ops Team
"""
import os
from thefuck.utils import for_app, which
from thefuck.shells import shell


@for_app('deploy')
def match(command):
    return (
        'environment not loaded' in command.output.lower()
        and os.path.isfile(os.path.expanduser('~/work/env.sh'))
    )


def get_new_command(command):
    # Returning a list yields multiple suggestions in priority order.
    return [
        shell.and_('source ~/work/env.sh', command.script),
        shell.and_('source ~/work/env.staging.sh', command.script),
    ]


def side_effect(old_cmd, new_command_str):
    # Optional. Runs BEFORE the new command is echoed to stdout.
    # Use sparingly — side effects are user-invisible cleanups.
    pass


# Optional metadata:
enabled_by_default = bool(which('deploy'))   # auto-disable on machines without the CLI
priority = 500                                # smaller = earlier
requires_output = True                        # default; set False if your rule
                                              # works without a rerun

Drop the file in ~/.config/thefuck/rules/. Next fuck invocation picks it up.

The corrector loop

def get_corrected_commands(command):
    corrected_commands = (
        corrected for rule in get_rules()
        if rule.is_match(command)
        for corrected in rule.get_corrected_commands(command))
    return organize_commands(corrected_commands)

A double generator: outer over rules, inner over each rule's emitted suggestions. organize_commands peeks the first command (so NoRuleMatched can bubble), then realises the rest, deduplicates by (script, side_effect), and emits in priority order. The peek-first design means the prompt can show suggestion #1 immediately while remaining rules are still being evaluated.

The shell adapter pattern

thefuck/shells/generic.py defines the abstract base. Subclasses override what they need:

Method Generic default Per-shell override
from_shell(script) calls _expand_aliases Fish overrides to fall back to fish -ic for non-binary aliases
to_shell(script) identity (no overrides)
app_alias(name) a one-line alias=eval... Bash, Zsh, Fish, Tcsh, PowerShell each emit a multi-line function with shell-specific syntax
instant_mode_alias(name) warns + falls back Bash and Zsh emit the script(1) recorder wrapper
get_aliases() {} Bash/Zsh parse TF_SHELL_ALIASES; Fish runs fish -ic alias/functions; Tcsh runs tcsh -ic alias
_get_history_file_name() '' Bash/Tcsh: HISTFILE env var; Zsh: ~/.zsh_history; Fish: ~/.config/fish/fish_history
_get_history_line(script) '' Each shell formats as needed (Tcsh prepends #+<ts>, Zsh prepends : ts:0;, Fish writes a YAML-style entry)
and_(*cmds) ' && '.join Fish: '; and '; PowerShell: -and-joined parens
or_(*cmds) ' || '.join Fish: '; or '
how_to_configure() None Each shell points at the appropriate rc file
_get_version() '' Each runs the shell with a version-printing command
put_to_history(script) no-op Fish appends to fish_history; others rely on the alias function's history -s/print -s
quote(s) shlex.quote (no overrides)
split_command(script) shlex with ?? placeholder for escaped spaces (no overrides)
get_builtin_commands() hard-coded list of POSIX builtins (no overrides)

The output reader pattern

thefuck/output_readers/__init__.py:get_output picks one of three implementations:

def get_output(script, expanded):
    if shell_logger.is_available():
        return shell_logger.get_output(script)
    if settings.instant_mode:
        return read_log.get_output(script)
    else:
        return rerun.get_output(script, expanded)

The default (rerun) is a 30-line wrapper around Popen plus psutil.Process.wait with timeout. Two subtleties:

  1. Process tree kill: on timeout, proc.children(recursive=True) is enumerated and each is killed before the parent. This avoids zombie GUI tools or backgrounded daemons.
  2. Encoding: stdout is decoded as utf-8 with errors='replace'. Bytes that don't decode become U+FFFD; rules should never assume clean ASCII.

read_log (instant mode) groups output by the USER_COMMAND_MARK (10 zero-width spaces) embedded in PS1 and reconstructs the visual display via pyte.Screen to handle wrapped lines.

shell_logger is the external-integration path: if SHELL_LOGGER_SOCKET env var points at a Unix socket, The Fuck connects, sends a JSON {"type": "list", "count": 5} request, and reads back a list of {"command": str, "output": str} for the last 5 commands. Used by recording wrappers like external session-recording integrations.

fastentrypoints.py

Vendored at the repo root. It monkey-patches setuptools' easy_install.ScriptWriter.get_args to emit a faster console-script template:

TEMPLATE = r'''\
# -*- coding: utf-8 -*-
# EASY-INSTALL-ENTRY-SCRIPT: '{3}','{4}','{5}'
__requires__ = '{3}'
import re
import sys

from {0} import {1}

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit({2}())

Notice no pkg_resources.require — that's the optimisation. On installs with many distributions on sys.path, pkg_resources.require can take 100+ ms.

Test strategy

tests/ mirrors the source layout. There are seven categories:

Test dir Focus
tests/ (root) argument_parser, conf, corrector, logs, readme, types, ui, utils
tests/entrypoints/ alias, fix_command, not_configured
tests/functional/ Docker-spawned shell sessions via pytest-docker-pexpect. Skipped by default; enabled via --enable-functional. Covers Bash, Fish, Tcsh, Zsh end-to-end.
tests/output_readers/ rerun
tests/rules/ One file per rule. Each tests match and get_new_command with mocked output.
tests/shells/ One file per shell adapter. Validates alias output, history file detection, app_alias_variables_correctly_set.
tests/specific/ git_support, sudo_support, npm helpers

The tests/conftest.py fixtures auto-disable memoization and the persistent cache, reset settings to defaults, and clear os.environ before every test (preserving only PATH). This isolation means tests that look at environment behaviour must explicitly set their env.

tests/Dockerfile is the basis for functional tests:

ARG PYTHON_VERSION
FROM python:${PYTHON_VERSION}
RUN apt-get update -y
RUN apt-get install -yy --no-install-recommends --no-install-suggests fish tcsh zsh
RUN pip install --upgrade pip
COPY . /src
RUN pip install /src

Code patterns and conventions

  • Decorators over inheritance. Rules compose from @for_app, @sudo_support, @git_support. There is no Rule base class to subclass.
  • Generator-heavy. The corrector chains generators end-to-end so the prompt can show suggestion #1 before suggestion #N is computed.
  • Early returns with short circuits. Most match() functions are a single return statement combining 2–4 checks.
  • script_parts over script.split(). Rules consistently use the lazy property so escaped spaces and quotes are handled.
  • re.findall + [0]. A common pattern in rules is re.findall(pattern, output)[0] — guarded by the match() having already verified the pattern is present. If both match and get_new_command extract the same regex, the typical pattern is to define a module-level compiled regex and use it in both.
  • @for_app is mandatory for app-specific rules. It both filters out non-matching apps and provides the at_least arg-count guard. Without it, you'd see lots of IndexError: list index out of range traces in fuck -d.
  • Six compatibility — from six.moves.urllib import parse, import six checks, etc. The project is straddling Python 2/3 even though Python 2 is deprecated.
  • No type hints. Type information is in docstrings as :type/:rtype for backward compat with Python 2.

Adding a new shell

  1. Subclass Generic in thefuck/shells/<name>.py.
  2. Override app_alias, friendly_name, _get_history_file_name, _get_history_line, how_to_configure, _get_version. Override and_/or_ if your shell uses non-&&/|| separators.
  3. Register in thefuck/shells/__init__.py:shells = {...}.
  4. Add tests under tests/shells/test_<name>.py mirroring the test_bash structure.
  5. If your shell supports instant mode, override instant_mode_alias.

Adding a new tool helper

Drop thefuck/specific/<tool>.py containing:

  • An <tool>_available = bool(which('<tool>')) flag.
  • Any cached helpers (@memoize/@cache).
  • Any decorator like @<tool>_support if rules need to consistently strip/wrap the tool name.

Then rules can do from thefuck.specific.<tool> import <tool>_available and set enabled_by_default = <tool>_available.

Potential improvements (visible in the source)

These are areas the source itself flags or that would benefit from a cleanup pass:

Area Observation Remediation
Python 2 compatibility six shims everywhere; setup.py lists python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*'. Drop Py2 once 3.5+ is the floor. The maintainer warned about this on Py2 in entrypoints/alias.py.
imp.load_source fallback Used when importlib.util is unavailable. Removed in Python 3.12 (issue #1527). Already handled with try/except; just drop the fallback for 3.12+.
distutils system/unix.py imports from distutils.spawn import find_executable — removed in 3.12 (issue #1444). Replace with shutil.which.
pkg_resources setup.py and get_installation_version use pkg_resources — deprecated in newer setuptools (issue #1552). Use importlib.metadata exclusively.
pyte<0.8.1 pin for Py2.7 extras_require pins pyte<0.8.1 and decorator<5 for Py2.7. Drop together with Py2 support.
CommandNotFound API apt_get rule has two code paths (Ubuntu 18.04+ and older). Consolidate when older versions are EOL.
Pattern lists The sudo rule's pattern list is hard-coded; new locale-specific permission errors require source changes. Could move to settings.py or a JSON file.
Test coverage for instant mode Instant mode is hard to test outside a real PTY; coverage is light. Functional tests exist for some shells but not for instant-mode interactions.
Cache eviction Cache never garbage-collects; entries are pinned by etag only. A periodic shelve.sync() and an LRU bound would be helpful for long-lived shells.
Alias regen cost thefuck --alias is invoked once per shell start; output is several KiB of shell code. Caching to disk would shave 30–80 ms. Consider an on-disk cache keyed by The Fuck version + shell version.
README detection The presence/absence of docs/README is currently invisible to The Fuck itself; CONTRIBUTING.md exists but is not surfaced to users via the --help. Cosmetic.

Contributing

Step Detail
Code style flake8-clean. The repo includes .editorconfig and a setup.cfg with project metadata.
Tests All new rules must include a tests/rules/test_<name>.py. Functional tests exist for shell adapters.
New rule checklist (1) match returns boolean, (2) get_new_command returns string or list, (3) priority set if non-default, (4) requires_output = False if rule keys only off command.script, (5) test file with at least one matching and one non-matching case.
PR target master branch.
Author Vladimir Iakovlev. Project status discussed in issues #1466 and #1566.

9. Integration Points

Inputs

Input Source Notes
The failing command command positional arg, or TF_HISTORY env, or shell fc -ln -10 Set by the alias before invocation
Captured output rerun subprocess, instant-mode log, or socket One of three readers
Shell aliases TF_SHELL_ALIASES (Bash/Zsh) or fish -ic alias (Fish) or tcsh -ic alias (Tcsh) Required for alias expansion in from_shell
Shell history shell-specific history file Read by history and path_from_history rules
Settings settings.py + env vars + flags Layered
User custom rules ~/.config/thefuck/rules/*.py Auto-loaded

Outputs

Output Sink Format
Corrected command stdout A single string (the alias evals it)
Confirmation prompt stderr Color-formatted with the USER_COMMAND_MARK prefix
Debug logs stderr [DEBUG], [WARN] prefixes
Shell alias stdout (when --alias used) Multi-line shell function source
Shell-logger output a tempfile (instant mode) or a Unix socket (external integration) Raw terminal bytes

Extension points

Extension API Discovery
User rules match / get_new_command Python module ~/.config/thefuck/rules/*.py
Third-party rule packs Same Any pip package whose name matches thefuck_contrib_* and contains a rules/ directory
Custom shell Subclass Generic and register in shells/__init__.py:shells Source modification needed
External output reader Implement is_available() and get_output(script) Set SHELL_LOGGER_SOCKET env var pointing at a Unix socket
EDITOR-based fix fix_file rule reads $EDITOR Must be set by the user
Per-tool decorators @for_app('toolname') Add new ones in thefuck/utils.py or in your rule file

External tools touched

The Fuck shells out to a long list of external tools — only the rules that need them. A non-exhaustive list:

Tool Used by Purpose
bash, zsh, fish, tcsh, powershell, pwsh shell adapters Version detection, alias listing
lsof port_already_in_use PID for a port
pkgfile pacman, pacman_not_found Arch package lookup
apt-get, apt-cache, apt apt rules Help text parsing
dnf, yum dnf/yum rules Help text parsing
brew brew rules Path prefix, command listing
docker docker_not_command Help text parsing
gem, npm, yarn, gradle, gradlew, mvn, lein, grunt, gulp, react-native, cargo, go, pyenv/rbenv/nodenv/goenv corresponding rules Help text / scripts listing
script instant mode PTY recorder
xdg-open (Linux), open (macOS), cmd /c start (Windows) open rule, yarn_help rule Open URLs in a browser
nix-env nixos_cmd_not_found Suggested install command

What The Fuck does NOT integrate with

To enumerate the negative knowledge set:

  • No HTTP server. No /health, no metrics endpoint.
  • No database. State is on disk: settings.py, shelve cache.
  • No daemon mode. Every invocation is a fresh Python process.
  • No GraphQL/REST/gRPC. No public API.
  • No webhooks. No outbound network calls.
  • No queueing or async. Single-threaded, synchronous.
  • No telemetry / analytics. No version-check ping.
  • No GUI. Terminal-only via colorama ANSI codes.
  • No language server / IDE plugin. CLI-first.
  • No WebSocket / SSE. No real-time channels.
  • No Kubernetes integration. No CRD, no operator. Run as a per-user CLI.
  • No SSO / OAuth. Inherits user's shell session.
  • No JSON output mode. thefuck always emits a shell-evalable string.
  • No man page bundled in source. Distro packages may add one. Help is via -h only.

Operational checklist for an enterprise rollout

  • [x] Audit ~/.config/thefuck/rules/ and any thefuck_contrib_* packages for malicious code paths.
  • [x] Set permissions: chmod 0700 ~/.config/thefuck && chmod 0600 ~/.config/thefuck/settings.py.
  • [x] Disable rm_root and keep git_push_force off (both are off by default).
  • [x] Set require_confirmation = True (default) for shared/production accounts.
  • [x] Distribute a curated rules = [...] whitelist via your dotfiles repo.
  • [x] Pre-populate slow_commands to cover your build-tool fleet.
  • [x] Decide whether instant mode is allowed (it persists output to /tmp).
  • [x] Pin a Python version (3.10 or 3.11) to avoid Py3.12+ imp / distutils issues.
  • [x] Document the alias line for every supported shell in your onboarding docs.
  • [x] If you ship a thefuck_contrib_<team> rule pack, version it independently and gate releases through code review.