# The Fuck — Administrator & Developer Guide

> 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 `eval`s 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

```text
                    ┌──────────────────────────────────────────────┐
                    │                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 `CorrectedCommand`s |
| 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 `eval`s 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 `eval`s 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`:

```python
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

```python
# ~/.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

```python
# 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']
```

```bash
export THEFUCK_NUM_CLOSE_MATCHES=5
```

#### Instant-mode setup

```bash
# In ~/.zshrc, MUST be the last line
eval "$(thefuck --alias --enable-experimental-instant-mode)"
```

```python
# ~/.config/thefuck/settings.py
instant_mode = True
```

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

```bash
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

```bash
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:

```bash
# 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:
> ```bash
> 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:

```bash
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](http://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`:

```python
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 `eval`s. 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:
```bash
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

```text
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`)

```python
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`)

```python
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:

```python
@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 | List[str]`. The correction. |
| `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`)

```python
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

```python
# ~/.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

```python
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:

```python
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:

```python
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:
```dockerfile
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](https://github.com/nvbn/thefuck/issues/1466) and [#1566](https://github.com/nvbn/thefuck/issues/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 `eval`s 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.

---

_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. Unofficial; not affiliated with or reviewed by the project._
