# The Fuck — User Guide

> **The Fuck** is a magnificent terminal companion that corrects errors in your previous console command. Type the wrong thing? Just type `fuck` and let it suggest — and run — the right one.

## 1. Quick Start

The Fuck reads the most recent failed command, runs it through a catalog of more than 160 correction rules, and offers the most likely fix. Press Enter to run it; press up/down to cycle through alternatives; press Ctrl+C to abort.

### Key features

- **Shell-agnostic alias.** Works in Bash, Zsh, Fish, Tcsh, and PowerShell — one command (`thefuck --alias`) emits the right wrapper for whichever shell you are in.
- **160+ built-in correction rules** for Git, Docker, npm, pip, Homebrew, apt, dnf, pacman, AWS CLI, Azure CLI, Heroku, Vagrant, Terraform, systemd, Maven, Gradle, Yarn, and many more.
- **Plugin architecture.** Drop a Python file into `~/.config/thefuck/rules/` and it joins the rotation. Third-party packages named `thefuck_contrib_*` are auto-discovered too.
- **Multi-suggestion UI.** When several rules match, browse the alternatives without retyping anything.
- **Side-effect aware.** Some fixes can do extra cleanup (delete a bad SSH key line, prune a half-extracted tarball) — the prompt marks those with `(+side effect)`.
- **History rewriting.** The corrected command is written into your shell history so the up-arrow brings up the fix, not the typo (Bash, Zsh, Fish, Tcsh).
- **Configurable via a Python file** at `~/.config/thefuck/settings.py` and via `THEFUCK_*` environment variables.

### Prerequisites

Python 3.5 or later (Python 2.7 is also supported but deprecated), a supported shell, and `pip` 6.0+.

### Install in one line

```bash
pip install --user thefuck
```

Then add the alias to your shell config and reload. For example, in `~/.zshrc`:

```bash
eval $(thefuck --alias)
```

> [!TIP]
> You can rename the alias to anything you like — `eval $(thefuck --alias FUCK)` gives you `FUCK` in caps; `eval $(thefuck --alias please)` gives a polite alternative. The Fuck respects the `TF_ALIAS` environment variable internally so all rules keep working.

### A first taste

```bash
$ git brnch
git: 'brnch' is not a git command. See 'git --help'.

The most similar command is
        branch
$ fuck
git branch [enter/↑/↓/ctrl+c]
* main
  feature/login
```

You typed `git brnch`, Git complained, you typed `fuck`, and the prompt now offers `git branch`. Hit Enter and it runs.

### What to try next

- Read **2. Installation** for package-manager-specific recipes (Homebrew, apt, pkg, scoop, AUR).
- See **4. Core Usage** for the full CLI flag reference and the keyboard controls in the suggestion prompt.
- Browse **5. Rule Catalog** to learn which mistakes The Fuck already knows how to fix.
- Skim **6. Common Tasks** for recipes — auto-confirm, retry on failure, instant-mode, custom rules.

---

## 2. Installation

### System requirements

| Requirement | Detail |
|---|---|
| Python | 2.7 or 3.5+ (Python 3.5+ recommended; 2.7 is deprecated) |
| `pip` | 6.0 or newer (older versions are explicitly rejected by `setup.py`) |
| Shell | Bash, Zsh, Fish, Tcsh/Csh, or PowerShell/PWSH |
| OS | Linux, macOS, Windows, BSD; FreeBSD/OpenBSD work via pkg/ports |
| Runtime deps | `psutil`, `colorama`, `six`; on Windows also `win_unicode_console` |
| Optional | `pyte` and a working `script(1)` binary for experimental instant mode |

### Install methods

=== "pip (any platform)"
    ```bash
    pip install --user thefuck
    ```

=== "Homebrew (macOS / Linux)"
    ```bash
    brew install thefuck
    ```

=== "Debian / Ubuntu"
    ```bash
    sudo apt update
    sudo apt install python3-dev python3-pip python3-setuptools
    pip3 install --user thefuck
    ```

=== "Fedora / RHEL"
    ```bash
    sudo dnf install python3-devel python3-pip
    pip3 install --user thefuck
    ```

=== "Arch Linux"
    ```bash
    sudo pacman -S thefuck
    ```

=== "FreeBSD"
    ```bash
    pkg install thefuck
    ```

=== "Source"
    ```bash
    git clone https://github.com/nvbn/thefuck.git
    cd thefuck
    pip install --user .
    ```

> [!IMPORTANT]
> `pip install --user` puts `thefuck` and `fuck` under `~/.local/bin` (Linux/macOS) or `%APPDATA%\Python\PythonX\Scripts` (Windows). If `fuck` is "not found" after install, that directory is not on your `$PATH` — see the troubleshooting entry below.

### Wire up the shell alias

The Fuck does not modify your shell config automatically on `pip install`. You add a single line, then reload.

=== "Bash"
    Append to `~/.bashrc` (or `~/.bash_profile` on macOS):
    ```bash
    eval "$(thefuck --alias)"
    ```
    Reload:
    ```bash
    source ~/.bashrc
    ```

=== "Zsh"
    Append to `~/.zshrc`:
    ```bash
    eval $(thefuck --alias)
    ```
    Reload:
    ```bash
    source ~/.zshrc
    ```

=== "Fish"
    Append to `~/.config/fish/config.fish`:
    ```fish
    thefuck --alias | source
    ```
    Reload:
    ```bash
    fish
    ```

=== "Tcsh / Csh"
    Append to `~/.tcshrc`:
    ```tcsh
    eval `thefuck --alias`
    ```
    Reload by exec'ing `tcsh`.

=== "PowerShell"
    Append to `$profile`:
    ```powershell
    iex "$(thefuck --alias)"
    ```
    Reload:
    ```powershell
    . $profile
    ```

> [!TIP]
> You can also let The Fuck configure itself: type `fuck` once with no alias set and it prints how-to-configure instructions. Type `fuck` a second time within 60 seconds (controlled by the internal `CONFIGURATION_TIMEOUT` constant) and it appends the alias line for you in supported shells. Then `source` the file or restart the shell.

### Verify the install

```bash
fuck --version
```

```text
The Fuck 3.32 using Python 3.11.4 and Bash 5.2.15
```

If you see the version banner — alias resolved, Python ABI is happy, and the shell detection landed on a known shell — installation is complete.

### Install troubleshooting

#### Error: `fuck: command not found` after install

**Cause:** `pip install --user` puts the script in a per-user directory that is not on `PATH` by default.

**Solution:**

1. Find the directory: `python3 -m site --user-base` then append `/bin` (Linux/macOS) or `\Scripts` (Windows).
2. Add it to `PATH`. On Bash/Zsh, append to your rc file:
   ```bash
   export PATH="$HOME/.local/bin:$PATH"
   ```
3. Restart the shell.

**Prevention:** Use `pipx install thefuck` if you prefer isolated CLIs, or install via Homebrew/apt/dnf where the package places binaries on `PATH` automatically.

#### Error: `ModuleNotFoundError: No module named 'imp'` (Python 3.12+)

**Cause:** Python 3.12 removed the legacy `imp` module that older releases of The Fuck imported. (Tracked in upstream issues [#1434](https://github.com/nvbn/thefuck/issues/1434) and [#1444](https://github.com/nvbn/thefuck/issues/1444).)

**Solution:**

1. Upgrade to the latest release: `pip install --user --upgrade thefuck`.
2. If still failing, downgrade your Python or use `pyenv install 3.11.x` and point `pip` at it.

**Prevention:** Pin a Python version known to work in your CI (3.10 or 3.11 are safest as of this writing).

#### Error: `ModuleNotFoundError: No module named 'distutils'` (Python 3.12+)

**Cause:** `distutils` was removed in Python 3.12; the `setup.py` install path uses `pkg_resources` which transitively pulls it in. (See issue [#1444](https://github.com/nvbn/thefuck/issues/1444).)

**Solution:**

1. `pip install --user setuptools` to bring back the shim.
2. Reinstall: `pip install --user --upgrade thefuck`.

#### Error: `pip older than 6.0 not supported`

**Cause:** `setup.py` refuses pip versions below 6.0.

**Solution:**

```bash
pip install -U pip
pip install --user thefuck
```

#### Error: `zsh: command not found: thefuck` (Termux / minimal Linux)

**Cause:** Termux installs Python scripts under `~/.local/bin` but the shell's `PATH` may not include it. Reported in issue [#650](https://github.com/nvbn/thefuck/issues/650) and [#1369](https://github.com/nvbn/thefuck/issues/1369).

**Solution:** Add `export PATH="$PREFIX/bin:$HOME/.local/bin:$PATH"` to `~/.zshrc` (Termux uses `$PREFIX` for the package root) and `source` it.

#### Error: PowerShell profile errors after adding the alias

**Cause:** Some PowerShell profiles run `thefuck --alias` synchronously on every shell start, which slows startup. Issue [#1380](https://github.com/nvbn/thefuck/issues/1380).

**Solution:** Wrap the alias init in a try/catch and consider caching the alias output:

```powershell
try {
    iex "$(thefuck --alias)"
} catch {
    Write-Warning "thefuck alias could not be loaded: $_"
}
```

#### Warning: `Config path ~/.thefuck is deprecated. Please move to ~/.config/thefuck`

**Cause:** Older releases used `~/.thefuck/`. The current release follows the XDG spec.

**Solution:**

```bash
mkdir -p ~/.config
mv ~/.thefuck ~/.config/thefuck
```

#### Error: WSL / Cygwin shell detection lands on `Generic` instead of Bash

**Cause:** The Fuck walks the parent process tree to detect the shell. In nested terminals (e.g., tmux inside WSL inside Windows Terminal) the parent name may be unrecognised.

**Solution:** Force it via env var: `export TF_SHELL=bash` (or `zsh`/`fish`/`tcsh`/`powershell`).

#### Error: `Shell logger supports only Linux and macOS`

**Cause:** `--shell-logger` uses `pty.fork()` and termios; that path is Linux/macOS only.

**Solution:** Drop the flag on Windows; instant mode is also Linux/macOS only.

#### Error: terminal misbehaves after `eval $(thefuck --alias)` on Zsh

**Cause:** The alias function exports `PYTHONIOENCODING`, manipulates `fc -ln`, and inserts a `print -s` for history rewriting. If a parent script captures stderr, the colored confirm prompt can leak. Reported in issue [#1476](https://github.com/nvbn/thefuck/issues/1476).

**Solution:** Make sure you `eval` the alias output from an interactive shell only — wrap the line:
```zsh
[[ $- == *i* ]] && eval $(thefuck --alias)
```

---

## 3. Configuration

The Fuck reads settings from three places, applied in this order (later sources win):

1. `~/.config/thefuck/settings.py` — a Python file auto-created on first run.
2. `THEFUCK_*` environment variables.
3. CLI flags (`-y`, `-d`, `-r`).

The file is plain Python — assign module-level names. The Fuck imports the file via `importlib.util` and reads only the keys it knows about.

### A minimal `settings.py`

```python
# ~/.config/thefuck/settings.py

# Only run these rules (default: all)
rules = ['git_push', 'git_pull', 'cd_correction', 'sudo']

# Or: keep defaults but disable specific noisy rules
exclude_rules = ['rm_root', 'sudo']

# Skip the y/n prompt — execute the top suggestion immediately
require_confirmation = False

# Push specific rules to higher priority (lower number = earlier)
priority = {'git_push': 100, 'no_command': 9999}

# Accept up to 5 close-match candidates per rule (default 3)
num_close_matches = 5
```

### Common option combinations

| Goal | Setting |
|---|---|
| Auto-run the first match without confirming | `require_confirmation = False` |
| Re-run `fuck` if the first fix also fails | `repeat = True` |
| Cap how far back to look in history | `history_limit = 2000` |
| Disable colors (CI / non-tty) | `no_colors = True` |
| Disable history rewriting | `alter_history = False` |
| Whitelist commands that are allowed to take longer | `slow_commands = ['gradle', 'mvn', './gradlew', 'vagrant']` |
| Stop searching specific PATH dirs (e.g., a slow NFS mount) | `excluded_search_path_prefixes = ['/mnt/slow']` |
| Override the wait timeout for re-running the failed command | `wait_command = 5` (seconds) |
| Override the wait timeout for "slow commands" | `wait_slow_command = 30` |
| Inject env vars into the rerun (default `{LC_ALL=C, LANG=C, GIT_TRACE=1}`) | `env = {'LC_ALL': 'C'}` |

> [!NOTE]
> Most settings are also exposed as `THEFUCK_*` environment variables so you can tune behavior without editing the file. See **9. Reference → Environment variables** for the full list.

### Where settings come from at runtime

```text
DEFAULT_SETTINGS  →  settings.py  →  THEFUCK_* env vars  →  CLI flags
```

If you ever want a fresh settings file, delete it and run `fuck` again — `~/.config/thefuck/settings.py` is regenerated with the default values commented out.

> [!TIP]
> For deeper architectural detail, the rule plugin protocol, the full settings dictionary, the env-var-to-setting map, and the alias function source code, see the **Admin Guide → Configuration Reference** and **Developer Internals**.

---

## 4. Core Usage

### The `fuck` alias vs. the `thefuck` binary

| Command | What it does |
|---|---|
| `fuck` | The shell function/alias the install instructions add. It captures `$PYTHONIOENCODING`, exports the recent shell history, calls `thefuck`, then `eval`s its stdout. This is what you should type. |
| `thefuck` | The Python entrypoint the alias wraps. You normally do not run it directly — its argument parsing is designed for the alias environment. |
| `thefuck_firstuse` (Windows) / `fuck` (Linux/macOS, when alias not configured) | The "not configured" splash. Prints the how-to-configure-alias message; second run within 60 seconds writes the alias line into your shell config automatically. |

### CLI flag reference

The Fuck accepts these options (all surface through both the alias and the raw `thefuck` binary):

| Flag | Type | Purpose |
|---|---|---|
| `-v`, `--version` | switch | Print `The Fuck X.Y using Python A.B.C and {shell name version}` and exit. |
| `-a [NAME]`, `--alias [NAME]` | optional value | Print the shell alias for the current shell. Optional argument lets you rename it (e.g., `--alias FUCK`). |
| `-l PATH`, `--shell-logger PATH` | value | Spawn a PTY-wrapped subshell that mirrors all output to `PATH`. Used internally by experimental instant mode. Linux/macOS only. |
| `--enable-experimental-instant-mode` | switch | Asks the alias to use instant mode (caches command output via shell logger so re-running the failed command is not necessary). Requires the `script` binary and Python 3. |
| `-h`, `--help` | switch | Print usage. |
| `-d`, `--debug` | switch | Verbose tracing — every rule import time, every `match` attempt, the full settings dump, and timing for `Total`, `Read output`, etc. |
| `--force-command CMD` | value | Hidden flag used internally by `--repeat` to retry a specific command without consulting history. |
| `command ...` | positional | The command to fix. Normally provided automatically by the alias from `fc -ln -10` (or PowerShell `Get-History -Count 1`). |
| `-y`, `--yes`, `--yeah`, `--hard` | switch | Mutually exclusive with `-r`. Skip the up/down/enter prompt — run the top suggestion immediately. |
| `-r`, `--repeat` | switch | Mutually exclusive with `-y`. If the corrected command also fails, run `fuck` again on its output. |

`-y` and `-r` cannot be combined — the parser puts them in a `mutually_exclusive_group` because chaining them is too dangerous (you could ladder destructive corrections without ever seeing a prompt).

### The interactive prompt

When more than one rule matches, you see something like:

```text
git push origin master --force-with-lease (+side effect) [enter/↑/↓/ctrl+c]
```

| Key | Action |
|---|---|
| Enter / Return | Run the highlighted suggestion. |
| ↑ or `k` (qwerty) or `e` (colemak) or Ctrl+N | Previous suggestion. |
| ↓ or `j` (qwerty) or `n` (colemak) or Ctrl+P | Next suggestion. |
| Ctrl+C or `q` | Abort — print `Aborted` and do nothing. |

> [!NOTE]
> Suggestions appear sorted by rule priority (lower number = higher priority — the prompt's first option is the rule with the smallest priority). You can re-rank rules in `settings.py` via the `priority` dict.

The `(+side effect)` suffix marks suggestions whose rule defined a `side_effect` callable. Side-effect-bearing rules in the catalog include `dirty_untar`, `dirty_unzip`, and `ssh_known_hosts` (which removes the conflicting host-key line from `~/.ssh/known_hosts`).

### How history rewriting works

When you accept a fix, The Fuck writes the corrected command to your shell history so the up-arrow recall is the right command. The implementation differs per shell:

- **Bash**: `history -s $TF_CMD`
- **Zsh**: `print -s $TF_CMD`
- **Fish**: deletes the wrong command (`builtin history delete --exact --case-sensitive`) and appends the fix to `~/.config/fish/fish_history`
- **Tcsh**: appends to `~/.history`
- **PowerShell / Generic**: no history mutation (PowerShell's history API is per-session)

Disable globally with `alter_history = False` in `settings.py` or `THEFUCK_ALTER_HISTORY=false`.

### Where The Fuck runs the failing command

By default, when the alias hands the script to `thefuck`, the engine **re-runs** the command in a subprocess with `LC_ALL=C LANG=C GIT_TRACE=1` to capture deterministic output, then feeds that output into the rule pipeline. There are three modes:

1. **Rerun mode** (default). The current command is re-executed. This is why corrections of long commands feel slow — `wait_command` (default 3 seconds) caps how long it waits, and `slow_commands` like `lein`, `react-native`, `gradle`, `./gradlew`, `vagrant` get the longer `wait_slow_command` (default 15 seconds).
2. **Instant mode** (`THEFUCK_INSTANT_MODE=true` or `--enable-experimental-instant-mode`). The alias replaces itself with a shell logger that records every command's output to a temp file. The Fuck then reads from that log instead of re-running. Only Bash and Zsh on Linux/macOS, and you need the `script(1)` binary.
3. **Shell logger mode** (auto). If a Unix socket is exposed at `$SHELL_LOGGER_SOCKET`, The Fuck talks to it and asks for the last 5 commands' output. This is for integrations that record their own shell sessions.

> [!WARNING]
> Instant mode is experimental, sets `THEFUCK_INSTANT_MODE=True`, modifies `PS1` to embed an invisible "user command mark", and replaces your shell with a `script` recording. If `PS1` is later overwritten by another tool (e.g., `oh-my-zsh` theme), instant mode silently falls back to rerun mode and prints a warning to stderr.

### Sudo handling

Many rules are wrapped with the `@sudo_support` decorator. If your script starts with `sudo `, the wrapper strips it before matching, runs the rule, and re-adds `sudo ` to the suggestion. So `sudo apt-get serch foo` matches the same `apt_get_search` rule as `apt-get serch foo`.

A separate `sudo` rule fires when the output contains any of ~25 phrases meaning "you need root" — `permission denied`, `must be root`, `requires superuser`, `you don't have access to the history db`, `EACCES`, `EdsPermissionError`, etc. — and prepends `sudo` (or `sudo sh -c "..."` if the script contains `&&` or `>`).

The opposite rule `unsudo` fires on `you cannot perform this operation as root` — it strips a leading `sudo`.

---

## 5. Rule Catalog

Rules are Python modules in `thefuck/rules/`. Each defines `match(command)` and `get_new_command(command)`. The bundled catalog ships **160+ rules** organised below by ecosystem. Rules apply automatically — there is nothing to enable per-rule unless you opted into a restricted `rules = [...]` list.

### Generic / shell built-ins

| Rule | Fires on | Suggests |
|---|---|---|
| `cd_correction` | `cd typo/path` with "no such file or directory" / "can't cd to" | Spell-checks each path component against actual sub-directories |
| `cd_mkdir` | `cd missing/dir` (any error) | `mkdir -p missing/dir && cd missing/dir` |
| `cd_parent` | `cd..` (typo, no space) | `cd ..` |
| `cd_cs` | `cs path` (one-key typo for `cd`) | `cd path` |
| `cat_dir` | `cat /some/dir` | `ls /some/dir` |
| `chmod_x` | `./script.sh` with "permission denied" | `chmod +x script.sh && ./script.sh` |
| `cp_create_destination` | `cp src dest/` when `dest/` does not exist | `mkdir -p dest && cp src dest/` |
| `cp_omitting_directory` | `cp src dest` complaining "is a directory" | `cp -a src dest` |
| `dry` | Doubled command (e.g., `git git status`) | Drop the duplicate (priority 900 — fires before `cd cd foo`) |
| `dirty_untar` | `tar -xf archive.tar.gz` extracting into the current dir | `mkdir -p archive && tar -xf archive.tar.gz -C archive`. Side effect: clean up loose files. |
| `dirty_unzip` | `unzip archive.zip` with multiple top-level files | `unzip archive.zip -d archive`. Side effect: delete loose files. |
| `fix_alt_space` | non-breaking space in the command (Alt+Space typo) | Replace with regular space |
| `fix_file` | Last command output names a file:line (Python, bash, JS, Go, Rust, etc.) | `$EDITOR file +line && original_command` (requires `$EDITOR` set; supports per-language patterns) |
| `has_exists_script` | `script_in_cwd` returns "command not found" | `./script_in_cwd` |
| `history` | Anything close-matchable to a previous command | The closest historic command (priority 9999 — last-resort) |
| `ls_all` | `ls` returning empty | `ls -A` |
| `ls_lah` | Any `ls` without flags | `ls -lah` |
| `man` | `man 3 foo` not found | Tries section 2, then `foo --help` |
| `man_no_space` | `manfoo` (missing space) | `man foo` |
| `mkdir_p` | `mkdir a/b/c` with "no such file or directory" | `mkdir -p a/b/c` |
| `missing_space_before_subcommand` | `gitstatus` (run-on command) | `git status` |
| `no_command` | First word not on `PATH` and resembles something on `PATH` | Closest match from history + `$PATH` |
| `no_such_file` | `mv src dst/missing/path` | `mkdir -p dst/missing && mv src dst/missing/path` |
| `open` | `open github.com` or "file does not exist" | `open http://github.com`, `touch file && open file`, or `mkdir file && open file` |
| `path_from_history` | "no such file or directory" with a path | Replace with a similar path that appears in shell history (priority 800 — fires before generic rules) |
| `quotation_marks` | Mixed `'` and `"` in script | Normalise to `"` |
| `remove_shell_prompt_literal` | Script starts with `$ ` (copy-paste from docs) | Drop the `$` prefix |
| `remove_trailing_cedilla` | Script ends with `ç` | Drop the trailing character |
| `rm_dir` | `rm dir` failing "is a directory" | `rm -rf dir` (or `rm -r` for hdfs) |
| `rm_root` | `rm /` without `--no-preserve-root` | `rm / --no-preserve-root` (disabled by default — opt-in via `rules`) |
| `sl_ls` | Just `sl` not found | `ls` |
| `sudo` | Any "permission denied" / "must be root" / "EACCES" message | `sudo <script>` (or `sudo sh -c "..."` for compound commands) |
| `sudo_command_from_user_path` | `sudo cmd` with "command not found" but `cmd` exists in user PATH | `sudo env "PATH=$PATH" cmd` |
| `unsudo` | "you cannot perform this operation as root" | Strip leading `sudo` |
| `switch_lang` | Garbled command typed in wrong keyboard layout (Russian, Ukrainian, Persian, Hebrew, Greek, Korean) | Re-encode to a Latin layout |
| `touch` | `touch path/missing/file` | `mkdir -p path/missing && touch path/missing/file` |
| `long_form_help` | Output mentions `--help` or `Run 'foo --help' for details` | Replace `-h` with `--help` or run the suggested command (priority 5000) |
| `wrong_hyphen_before_subcommand` | `tool-sub` (hyphen instead of space) | `tool sub` (priority 4500, no output required) |

### Git

| Rule | Fires on | Suggests |
|---|---|---|
| `git_add` | `git checkout/diff/etc. file` with "did not match any file(s)" — and the file actually exists | `git add -- file && original` |
| `git_add_force` | `git add` with "Use -f if you really want to add them" | `git add --force` |
| `git_bisect_usage` | `git bisect typo` with usage banner | Closest of `start`, `bad`, `good`, `skip`, `next`, `reset`, `visualize`, `replay`, `log`, `run`, `terms` |
| `git_branch_0flag` | `git branch 0d typo-branch` (zero typed for hyphen) | Replace `0` with `-` (e.g., `-d`); handles "branch already exists" by adding `git branch -D` |
| `git_branch_delete` | `git branch -d` with "If you are sure you want to delete it" | `git branch -D` |
| `git_branch_delete_checked_out` | `git branch -d/-D feature` failing "Cannot delete branch checked out at" | `git checkout master && git branch -D feature` |
| `git_branch_exists` | `git branch newbranch` with "already exists" | Five alternatives: `branch -d && branch`, `branch -d && checkout -b`, `branch -D && branch`, `branch -D && checkout -b`, plain `git checkout newbranch` |
| `git_branch_list` | `git branch list` (instead of plain `git branch`) | `git branch --delete list && git branch` |
| `git_checkout` | "did not match any file(s)" on checkout (no "git add?" hint) | Closest existing branch, or `git branch X && git checkout X`, or `git checkout -b` |
| `git_clone_git_clone` | `git clone git clone URL` (typo) | Drop the duplicate `git clone ` |
| `git_clone_missing` | A bare git URL pasted without `git clone` | `git clone URL` |
| `git_commit_add` | `git commit` with "no changes added" | `git commit -a` and `git commit -p` |
| `git_commit_amend` | Any `git commit` | `git commit --amend` |
| `git_commit_reset` | Any `git commit` | `git reset HEAD~` |
| `git_diff_no_index` | `git diff fileA fileB` (two files outside repo) | `git diff --no-index fileA fileB` |
| `git_diff_staged` | `git diff` (without `--staged`) | `git diff --staged` |
| `git_fix_stash` | `git stash typo` with usage banner | Closest of `apply`, `branch`, `clear`, `drop`, `list`, `pop`, `save`, `show`; falls back to `git stash save typo` |
| `git_flag_after_filename` | "fatal: bad flag X used after filename" or "option must come before non-option args" | Reorder flags before filename |
| `git_help_aliased` | `git help X` where X is "aliased to ..." | `git help <real-command>` |
| `git_hook_bypass` | `git am/commit/push ...` | Add `--no-verify` (priority 1100; no output required — opt-in by users with strict pre-commit setups) |
| `git_lfs_mistype` | `git lfs typo` with "Did you mean this?" | Closest LFS subcommand |
| `git_main_master` | Output mentions `'master'` or `'main'` | Swap one for the other (priority 1200) |
| `git_merge` | `git merge X` with "not something we can merge" + "Did you mean this?" | The suggested remote branch |
| `git_merge_unrelated` | `git merge X` with "refusing to merge unrelated histories" | Append `--allow-unrelated-histories` |
| `git_not_command` | `git typo` with "is not a git command. See 'git --help'" + similar list | Closest similar git command |
| `git_pull` | `git pull` with "set-upstream" hint | Run the suggested set-upstream command, then re-pull |
| `git_pull_clone` | `git pull` with "Not a git repository" + "Stopping at filesystem boundary" | `git clone` |
| `git_pull_uncommitted_changes` | `git pull` with "unstaged changes" / "uncommitted changes" | `git stash && git pull && git stash pop` |
| `git_push` | `git push` with "git push --set-upstream" message | The exact `--set-upstream` line, preserving any extra args |
| `git_push_different_branch_names` | `git push` with "upstream branch does not match" | The recommended `git push <remote> <branch>` |
| `git_push_force` | `git push` rejected because branch is behind | `git push --force-with-lease` (disabled by default — opt-in) |
| `git_push_pull` | `git push` rejected because remote is ahead | `git pull && git push` |
| `git_push_without_commits` | "src refspec X does not match any" | `git commit -m "Initial commit" && git push` |
| `git_rebase_merge_dir` | "rebase-merge directory" already exists | `git rebase --continue/--abort/--skip` |
| `git_rebase_no_changes` | `git rebase --continue` with "No changes" | `git rebase --skip` |
| `git_remote_delete` | `git remote delete name` | `git remote remove name` |
| `git_remote_seturl_add` | `git remote set-url name URL` with "No such remote" | `git remote add name URL` |
| `git_rm_local_modifications` | `git rm file` with "local modifications" | `git rm --cached file` and `git rm -f file` |
| `git_rm_recursive` | `git rm dir` with "without -r" | `git rm -r dir` |
| `git_rm_staged` | `git rm file` with "changes staged in the index" | `git rm --cached file` and `git rm -f file` |
| `git_stash` | Output contains "or stash them" | `git stash && original-command` |
| `git_stash_pop` | `git stash pop` with overwrite warning (priority 900) | `git add --update && git stash pop && git reset .` |
| `git_tag_force` | `git tag X` with "already exists" | `git tag --force X` |
| `git_two_dashes` | "did you mean `--option`" | Replace `-option` with `--option` |
| `scm_correction` | `git ...` in an `hg` repo or vice versa | Swap to the right SCM |

> [!NOTE]
> All Git rules use the `@git_support` decorator. It also handles GitHub's `hub` wrapper (`alias git=hub`) and resolves git aliases by parsing `trace: alias expansion: ...` lines from output (set via the default `env = {'GIT_TRACE': '1'}`).

### Docker, Kubernetes-adjacent, and registries

| Rule | Fires on | Suggests |
|---|---|---|
| `docker_not_command` | "is not a docker command" or `Usage:	docker` banner | Closest `docker` (or management) subcommand |
| `docker_image_being_used_by_container` | `docker rmi` with "image is being used by running container" | `docker container rm -f <id> && original` |
| `docker_login` | "access denied" + "may require 'docker login'" | `docker login && original` |

### Package managers

| Rule | Fires on | Suggests |
|---|---|---|
| `apt_get` | `cmd: not found` / `not installed` and the system has `CommandNotFound` | `sudo apt-get install <pkg> && original` |
| `apt_get_search` | `apt-get search foo` | `apt-cache search foo` |
| `apt_invalid_operation` | `apt`/`apt-get`/`apt-cache` with "Invalid operation" | Closest valid op (`uninstall` → `remove` shortcut) |
| `apt_list_upgradable` | `apt` output suggests `apt list --upgradable` | Run `apt list --upgradable` |
| `apt_upgrade` | After listing upgradable packages | `apt upgrade` |
| `brew_install` | `brew install foo` with "No available formula. Did you mean ..." | `brew install <suggestion>` for each candidate |
| `brew_link` | `brew ln/link` with "brew link --overwrite --dry-run" hint | Insert `--overwrite --dry-run` |
| `brew_reinstall` | `brew install foo` already installed and up-to-date | `brew reinstall foo` |
| `brew_uninstall` | `brew uninstall/rm/remove` with "brew uninstall --force" hint | Add `--force` |
| `brew_unknown_command` | `brew typo` with "Unknown command" | Closest from `Library/Homebrew/cmd` and tap-specific commands |
| `brew_update_formula` | `brew update foo` (formula vs. brew itself) | `brew upgrade foo` |
| `brew_cask_dependency` | `brew install` requiring cask dependencies | `brew cask install dep1 && brew cask install dep2 && original` |
| `cargo` | Bare `cargo` (no subcommand) | `cargo build` |
| `cargo_no_command` | `cargo typo` with "no such subcommand" + "Did you mean" | The suggested cargo subcommand |
| `choco_install` | `choco install foo` with "Installing the following packages" | `choco install foo.install` |
| `composer_not_command` | `composer typo` with "Did you mean this?" | Suggested command, or `install` → `require` |
| `conda_mistype` | `conda typo` with "Did you mean 'conda ...'" | The suggested conda command |
| `dnf_no_such_command` | `dnf typo` with "No such command" | Closest dnf operation |
| `gem_unknown_command` | `gem typo` with "Unknown command" | Closest from `gem help commands` |
| `pip_install` | `pip install foo` with "Permission denied" | First try `--user`, then fall back to `sudo` |
| `pip_unknown_command` | `pip typo` with "unknown command, maybe you meant" | The suggested pip command |
| `npm_missing_script` | `npm run foo` with "missing script: foo" | Closest from `npm run-script` listing |
| `npm_run_script` | `npm foo` where `foo` is in `package.json` scripts | `npm run-script foo` |
| `npm_wrong_command` | `npm typo` with "where <command> is one of" | Closest npm command |
| `pacman` | "not found" with `pkgfile` available | `pacman/yay/pikaur/yaourt -S <package> && original` |
| `pacman_invalid_option` | `pacman` complaining "invalid option '-x'" with a lowercase short flag | Uppercase that flag |
| `pacman_not_found` | `pacman/yay/pikaur/yaourt -S` with "target not found" | Replace package name with `pkgfile` lookup |
| `yum_invalid_operation` | `yum typo` with "No such command" | Closest yum op (`uninstall` → `remove`) |
| `yarn_alias` | `yarn typo` with "Did you mean" | The suggested yarn command |
| `yarn_command_not_found` | `yarn typo` with "Command not found" | Closest from `yarn --help` (with `require`→`add` map) |
| `yarn_command_replaced` | `yarn` output with "Run 'X' instead" | Run X |
| `yarn_help` | `yarn help cmd` with "for documentation about this command" | Open the doc URL in default browser |
| `nixos_cmd_not_found` | "command-not-found" hint with a `nix-env -iA pkg` line | `nix-env -iA pkg && original` |
| `omnienv_no_such_command` | `pyenv/rbenv/nodenv/goenv` with "env: no such command" | Common typos (`list` → `versions`, `remove` → `uninstall`) and closest from `<env> commands` |
| `python_module_error` | `ModuleNotFoundError: No module named 'X'` | `pip install X && original` |

### Cloud / DevOps CLIs

| Rule | Fires on | Suggests |
|---|---|---|
| `aws_cli` | `aws ...` with "Invalid choice ... maybe you meant" | One suggestion per option in the list |
| `az_cli` | `az ...` with "is not in the X command group" | Each "most similar choice" |
| `heroku_multiple_apps` | Output mentions `multiple-environments` doc | Re-run with `--app <name>` for each app |
| `heroku_not_command` | "Run heroku _ to run" | The suggested heroku command |
| `terraform_init` | "this module is not yet installed" / "initialization required" | `terraform init && original` |
| `terraform_no_command` | "Terraform has no command named X. Did you mean Y?" | Replace X with Y |
| `tsuru_login` | "not authenticated" + "session has expired" | `tsuru login && original` |
| `tsuru_not_command` | `tsuru typo` with "Did you mean?" | Suggested tsuru commands |
| `vagrant_up` | Output suggests "run `vagrant up`" | `vagrant up && original` (and `vagrant up <machine> && original` if a machine is implied) |
| `gradle_no_task` | `gradle` / `gradlew` with "Task X is ambiguous/not found" | Closest from `gradle tasks` |
| `gradle_wrapper` | `gradle ...` not on PATH but `./gradlew` exists | `./gradlew args` |
| `grunt_task_not_found` | `grunt task` with "Warning: Task X not found" | Closest from `grunt --help` |
| `gulp_not_task` | `gulp X` with "is not in your gulpfile" | Closest from `gulp --tasks-simple` |
| `lein_not_task` | `lein typo` with "is not a task. See 'lein help'" | Closest similar Leiningen task |
| `mvn_no_command` | `mvn` with "No goals have been specified" | `mvn clean package` and `mvn clean install` |
| `mvn_unknown_lifecycle_phase` | `mvn typo` with "Unknown lifecycle phase" | Closest from "Available lifecycle phases" |
| `react_native_command_unrecognized` | `react-native typo` with "Unrecognized command" | Closest from `react-native --help` |

### Languages & build tools

| Rule | Fires on | Suggests |
|---|---|---|
| `ag_literal` | `ag pattern` with "run ag with -Q" | `ag -Q pattern` |
| `cpp11` | g++/clang++ with "C++ 2011" / `-Wc++11-extensions` | Append `-std=c++11` |
| `go_run` | `go run foo` (no `.go`) | `go run foo.go` |
| `go_unknown_command` | `go typo` with "unknown command" | Closest from `go` help |
| `grep_arguments_order` | `grep <file> <pattern>` failing | Move file to the end |
| `grep_recursive` | `grep pattern dir/` failing "is a directory" | `grep -r pattern dir/` |
| `java` | `java foo.java` (running compiled code, suffix wrong) | `java foo` (drops `.java`) |
| `javac` | `javac foo` (no `.java`) | `javac foo.java` |
| `php_s` | `php -s ...` (lowercase) | `php -S ...` |
| `python_command` | `./script.py` with "Permission denied" / "command not found" | `python script.py` |
| `python_execute` | `python foo` (no `.py`) | `python foo.py` |
| `test.py` (file `test.py.py`) | `test.py` not found | `pytest` (priority 900 — beats `python_command`) |
| `prove_recursively` | `prove dir/` with "NOTESTS" | `prove -r dir/` |

### Networking, SSH, and system utilities

| Rule | Fires on | Suggests |
|---|---|---|
| `ifconfig_device_not_found` | `ifconfig` with "Device not found" | Closest from `ifconfig -a` |
| `ssh_known_hosts` | `ssh`/`scp` with "REMOTE HOST IDENTIFICATION HAS CHANGED!" / DNS-spoofing warnings | Re-run the command. Side effect: remove the offending line(s) from the named known_hosts file. |
| `port_already_in_use` | `EADDRINUSE` / "Address already in use" patterns | `kill <pid> && original` (uses `lsof -i :PORT`) |
| `tmux` | `tmux typo` with "ambiguous command, could be:" | One per suggestion |
| `whois` | `whois URL` or `whois sub.example.com` | Strip path / drop subdomain |
| `hostscli` | `hostscli ...` with "No such command" / `WebsiteImportError` | Closest of `block`, `unblock`, `websites`, `block_all`, `unblock_all` |
| `systemctl` | `systemctl typo` with mis-ordered args | Swap last two args |
| `unknown_command` | "X: Unknown command. Did you mean Y?" | The suggested command |
| `workon_doesnt_exists` | `workon env` for an env that doesn't exist | Closest from `~/.virtualenvs/`, plus `mkvirtualenv env` |
| `mercurial` | `hg typo` with "did you mean one of" / "is ambiguous" | Closest hg command |
| `fab_command_not_found` | `fab task` with "Warning: Command(s) not found" | Closest from "Available commands:" |
| `sed_unterminated_s` | `sed "s/foo/bar"` (missing trailing slash) | Add the closing `/` |

### Mobile development

| Rule | Fires on | Suggests |
|---|---|---|
| `adb_unknown_command` | `adb typo` with "Android Debug Bridge version" banner | Closest of 35+ `adb` commands |

### Web / scripting frameworks

| Rule | Fires on | Suggests |
|---|---|---|
| `django_south_ghost` | `manage.py migrate` with "or pass --delete-ghost-migrations" | Append `--delete-ghost-migrations` |
| `django_south_merge` | `manage.py migrate` with "--merge: will just attempt the migration" | Append `--merge` |
| `rails_migrations_pending` | "Migrations are pending. To resolve this issue, run: ..." | Run the suggested rake/rails command, then re-run |
| `ln_no_hard_link` | `ln dir target` with "hard link not allowed for directory" | `ln -s dir target` |
| `ln_s_order` | `ln -s target source` (wrong order, "File exists") | Swap positional args |

### Custom & history-based fallback rules (lowest priority — last-resort)

| Rule | Priority | Notes |
|---|---|---|
| `path_from_history` | 800 | Replaces a missing path with one that appears in your shell history |
| `cd_cs` | 900 | Single-key `cs` typo for `cd` |
| `git_stash_pop` | 900 | Comes before generic stash rules |
| `dry` | 900 | Doubled command (`git git status` → `git status`) |
| `test.py` | 900 | `test.py` not found → `pytest` |
| `git_hook_bypass` | 1100 | Add `--no-verify` to `am`/`commit`/`push` |
| `git_main_master` | 1200 | Swap `master` ↔ `main` |
| `man_no_space` | 2000 | `manfoo` → `man foo` |
| `no_command` | 3000 | Closest executable for typo'd commands |
| `missing_space_before_subcommand` | 4000 | `gitstatus` → `git status` |
| `wrong_hyphen_before_subcommand` | 4500 | `git-status` → `git status` |
| `long_form_help` | 5000 | `cmd -h` → `cmd --help` (or what the help banner suggests) |
| `history` | 9999 | Closest match from valid shell history (very last resort) |

> [!TIP]
> The full list above is the bundled catalog. To see what is actually loaded on **your** system at runtime, run `fuck -d` (debug). Each rule's import is logged with timing. Rules that fail to import (missing optional dependency, syntax error in a user rule) are reported as `[WARN] Rule X failed to load:` followed by the traceback — they are skipped, not fatal.

### Authoring your own rule

Drop a `~/.config/thefuck/rules/foo.py` with two functions:

```python
# ~/.config/thefuck/rules/uppercase.py
def match(command):
    return command.script.startswith('PYTHON ')

def get_new_command(command):
    return command.script.replace('PYTHON', 'python', 1)

# Optional metadata:
# enabled_by_default = True
# priority = 1000
# requires_output = True   # if False, runs even when there is no output
# def side_effect(old_cmd, new_command_str): ...
```

The Fuck imports the file with `importlib.util.spec_from_file_location`, so the only requirement is that `match` and `get_new_command` are top-level callables. If `get_new_command` returns a list instead of a string, the prompt offers each entry as a separate suggestion (priorities are scaled by their order).

For deeper details on the Rule lifecycle, the `Command` object, the `@for_app` / `@sudo_support` / `@git_support` decorators, side effects, the `@cache` decorator, and the third-party `thefuck_contrib_*` discovery mechanism, see the **Admin Guide → Developer Internals**.

---

## 6. Common Tasks

### Recipe: Auto-confirm — execute the top suggestion immediately

```bash
fuck -y
```

```text
git push --set-upstream origin master
[main 39e5b1a] ...
```

Works once. To make it the default everywhere, add `require_confirmation = False` to `~/.config/thefuck/settings.py` or export `THEFUCK_REQUIRE_CONFIRMATION=false`.

> [!WARNING]
> Auto-confirm is dangerous in scripts that produce ambiguous matches. The Fuck always picks the rule with the smallest priority — if two rules match equally, the alphabetic tiebreaker is the rule filename. For destructive corrections (rm, force-push, ssh known_hosts cleanup), keep the prompt on.

### Recipe: Retry until something works

```bash
fuck -r
```

If the corrected command itself fails, The Fuck wraps the suggestion in `… || fuck --repeat --force-command "<orig>"`, so it loops until either a fix succeeds or every rule is exhausted.

You cannot combine `-y` and `-r` (the parser refuses).

### Recipe: Rename the alias

If `fuck` is too profane for your workplace:

```bash
# In your shell rc:
eval $(thefuck --alias FUCK)
# or:
eval $(thefuck --alias please)
# or even:
eval $(thefuck --alias damnit)
```

The Fuck's internal `TF_ALIAS` env var follows the rename, and the "no fucks given" error message becomes "Nothing found" if the alias is anything other than `fuck`.

### Recipe: Disable noisy rules

If `sudo` keeps suggesting itself for non-sudo problems:

```python
# settings.py
exclude_rules = ['sudo', 'no_command']
```

Or per-environment:

```bash
export THEFUCK_EXCLUDE_RULES="sudo:no_command"
```

### Recipe: Whitelist only a small subset

```python
# settings.py
rules = ['git_push', 'git_pull', 'cd_correction', 'cd_mkdir', 'mkdir_p']
```

Combine with `DEFAULT_RULES` to keep all defaults but pin extras:

```bash
export THEFUCK_RULES="DEFAULT_RULES:rm_root:git_push_force"
```

### Recipe: Boost a rule's priority so it always wins

```python
# settings.py — smaller numbers fire first
priority = {
    'git_push': 100,        # always before all other git push fixes
    'history':  9999,       # keep history as last-resort
    'no_command': 9000,
}
```

Or via env:

```bash
export THEFUCK_PRIORITY="git_push=100:history=9999"
```

### Recipe: Tame slow commands (avoid the 3-second cutoff)

The default rerun timeout is 3 seconds; any command in `slow_commands` gets `wait_slow_command` (15s).

```python
# settings.py
slow_commands = ['gradle', './gradlew', 'mvn', 'lein', 'react-native', 'vagrant', 'docker-compose']
wait_slow_command = 30
```

Or:

```bash
export THEFUCK_SLOW_COMMANDS="gradle:./gradlew:mvn:lein:react-native:vagrant"
export THEFUCK_WAIT_SLOW_COMMAND=30
```

### Recipe: Inject env vars during the rerun

The default is `{LC_ALL: C, LANG: C, GIT_TRACE: 1}` — `LC_ALL=C` makes errors deterministic across locales, and `GIT_TRACE=1` lets git rules see alias expansions. To add (not replace) entries:

```python
# settings.py
env = {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1', 'NO_COLOR': '1'}
```

### Recipe: Use The Fuck inside Docker / inside a CI pipeline

CI typically lacks a tty, so the prompt would hang. Use:

```bash
export THEFUCK_REQUIRE_CONFIRMATION=false
export THEFUCK_NO_COLORS=true
fuck
```

Or run the binary directly bypassing the alias:

```bash
thefuck --yes <last command line here>
```

### Recipe: Try experimental instant mode (Bash/Zsh + Linux/macOS)

Instant mode skips the rerun by recording every command's output to a temp log:

```bash
# In your shell rc, replace the eval line with:
eval "$(thefuck --alias --enable-experimental-instant-mode)"
```

Requirements:

- Python 3 (instant mode warns and bails on Python 2).
- The `script(1)` binary on `PATH`.
- A `PS1` that does not get rewritten after The Fuck initialised it (oh-my-zsh themes that re-set `PS1` on every prompt break instant mode silently).
- Bash or Zsh — Fish/Tcsh/PowerShell fall back to rerun mode with a warning.

### Recipe: Disable history rewriting

Some setups don't want their history mutated:

```python
# settings.py
alter_history = False
```

The corrected command still runs; it just won't appear via up-arrow.

### Recipe: Migrate from `~/.thefuck/` to `~/.config/thefuck/`

```bash
mkdir -p ~/.config
mv ~/.thefuck ~/.config/thefuck
```

The Fuck will use the legacy path with a `Config path … is deprecated` warning if both exist; once migrated, the warning disappears.

### Recipe: Add a third-party rule pack

Any pip package whose name starts with `thefuck_contrib_` and that exposes a `rules/` sub-folder is discovered automatically:

```bash
pip install --user thefuck_contrib_my_company
```

The corrector's `get_rules_import_paths()` walks every `sys.path` entry and globs `thefuck_contrib_*/rules/`.

### Recipe: Force a specific shell

If shell auto-detection fails (nested terminals, exotic terminals, embedded PTYs):

```bash
export TF_SHELL=zsh    # one of: bash, zsh, fish, csh, tcsh, powershell, pwsh
```

### Recipe: Fix a file at a specific line (the `fix_file` rule)

```bash
$ python my_script.py
  File "my_script.py", line 42
    bad_syntax(
              ^
SyntaxError: unexpected EOF
$ fuck
vim my_script.py +42 && python my_script.py [enter/↑/↓/ctrl+c]
```

The `fix_file` rule recognises Python, Bash, JS/Node, Cargo/Rust, Go, GHC/Haskell, Make, Ruby, Zsh, Lua, Awk, Git config, LLC, Perl, and Fish error formats. Requires `$EDITOR` to be set; respects `fixlinecmd` and `fixcolcmd` settings if you want a non-default invocation.

---

## 7. Troubleshooting

A diagnostic decision tree:

1. Does `thefuck --version` print? → install OK.
2. Does `fuck` (with the alias) print "no fucks given"? → alias OK, no rule matched (or every rule that matched produced an empty suggestion).
3. Does `fuck -d` show `Importing rule X failed`? → bad rule file, fix that first.
4. Is the wrong suggestion winning? → adjust `priority` in `settings.py`.
5. Is the corrected command itself failing? → use `fuck -r`.

### Error: `No fucks given`

**Cause:** No rule's `match()` returned True. Either nothing in the catalog handles your command, or your output cache is empty (instant mode misconfigured).

**Solution:**

1. Run `fuck -d` to see which rules tried and what they saw.
2. Confirm the alias actually exported `TF_HISTORY` — the alias should set it to `$(fc -ln -10)`.
3. If you customised `rules = [...]`, add the rule that should match.

**Prevention:** Author a custom rule (see Admin Guide → Developer Internals) for command patterns you hit often.

### Error: `Nothing found`

**Cause:** Same as above, but you renamed the alias — the friendly "no fucks given" only fires when the alias is literally `fuck`.

### Error: `Seems like fuck alias isn't configured!`

**Cause:** You ran `fuck` but the alias is not exported. The "not-configured" entrypoint took over.

**Solution:**

1. Run the appropriate `eval $(thefuck --alias)` for your shell.
2. Save it to your shell rc and `source` the file.
3. Or run `fuck` a second time within 60 seconds — The Fuck auto-writes the alias if it can detect your shell config.

### Error: `Shell logger doesn't support your platform.`

**Cause:** `--shell-logger` (or instant mode) tried to start on Windows, FreeBSD, or a stripped-down Linux without a `$SHELL` env var.

**Solution:** Drop `--enable-experimental-instant-mode` from your alias line; instant mode is Linux/macOS only.

### Error: `Output log isn't specified` (instant mode)

**Cause:** Instant mode launched but `THEFUCK_OUTPUT_LOG` was not exported. The shell logger sub-process did not start cleanly.

**Solution:**

1. Verify the `script(1)` binary is on `PATH`.
2. Re-source your shell rc.
3. If the warning persists, drop instant mode — rerun mode is the safer default.

### Error: `PS1 doesn't contain user command mark`

**Cause:** Instant mode embeds an invisible marker (`U+200B` × 10) in `PS1`. Another tool (oh-my-zsh, starship, powerlevel10k) overwrote `PS1` after init.

**Solution:**

1. Make the `eval $(thefuck --alias --enable-experimental-instant-mode)` line the **last** line in your rc.
2. Or disable instant mode and use the default rerun mode.

### Error: `Removing possibly out-dated cache`

**Cause:** Switched Python versions; the on-disk `shelve` cache uses an incompatible dbm format.

**Solution:** Harmless — The Fuck deletes `~/.cache/thefuck` and rebuilds it. Look in `XDG_CACHE_HOME` (default `~/.cache`) if you want to clear it manually.

### Error: `Can't load settings from file`

**Cause:** `~/.config/thefuck/settings.py` raised an exception during import.

**Solution:**

1. Run `fuck -d` and read the printed traceback.
2. Common culprit: typo'd a setting name (e.g., `excluded_rules` instead of `exclude_rules`).
3. Worst case, delete the file — The Fuck regenerates a defaults-only template on next run.

### Error: `'PATH' environment variable not set` / no executables discovered

**Cause:** Some sandboxed runtimes (Docker `--env-clean`, systemd unit without `Environment=PATH`) clear `$PATH`. The `no_command` rule and the alias both depend on it.

**Solution:** Set `$PATH` in the environment before running the alias.

### Error: `Can't update history` (Fish)

**Cause:** `~/.config/fish/fish_history` is read-only or on a read-only FS.

**Solution:** `chmod u+w ~/.config/fish/fish_history`. Or set `alter_history = False`.

### Error: Suggestion includes the wrong sudo behaviour

**Cause:** The `sudo` rule's pattern list can match unexpected output (e.g., a docs string containing "permission denied").

**Solution:** Add `sudo` to `exclude_rules`, or boost the priority of the rule you want to win:

```python
# settings.py
priority = {'git_push': 100}   # win over sudo
```

### Error: Shell hangs after `fuck`

**Cause:** The rerun is waiting on a long-running command (the failed one) to finish. Default timeout is 3 seconds; the slow-command list adds 15.

**Solution:** Press Ctrl+C — The Fuck kills the process tree. Add the binary to `slow_commands` so future runs use the longer timeout.

### Error: `[WARN] Rule X failed to load`

**Cause:** A custom or contrib rule raised an exception at import time.

**Solution:**

1. Run `python -c "import sys; sys.path.insert(0, '~/.config/thefuck/rules'); import X"` to surface the import error.
2. Either fix the file or move it out of `~/.config/thefuck/rules/`.

### Error: `[WARN] Could not determine shell version`

**Cause:** `_get_version()` shelled out (e.g., `bash -c 'echo $BASH_VERSION'`) and the subprocess errored.

**Solution:** Cosmetic — `fuck --version` will just say "Bash" instead of "Bash 5.2". Investigate by running the underlying command yourself.

### Error: Mac Termux "termux errors, but thefuck works"

**Cause:** Reported as issue [#1369](https://github.com/nvbn/thefuck/issues/1369). Termux's path conventions throw spurious warnings during alias init.

**Solution:** Cosmetic; ignore or redirect stderr in your rc:
```bash
eval "$(thefuck --alias)" 2>/dev/null
```

### Error: `Warning: thefuck-fixed`

**Cause:** Tracked in issue [#1570](https://github.com/nvbn/thefuck/issues/1570) — a third-party command produces an unparseable output that triggers a misclassified rule.

**Solution:** Run with `-d` to see which rule matched; add it to `exclude_rules`.

### Error: `Use of deprecated / removed pkg_resources`

**Cause:** `setup.py` uses `pkg_resources.get_distribution` which is deprecated in newer setuptools (issue [#1552](https://github.com/nvbn/thefuck/issues/1552)).

**Solution:** Cosmetic during install. Upgrading setuptools to a recent release silences it. Does not break runtime.

### Error: `libexpat now an explicit install requirement on macOS`

**Cause:** Issue [#1560](https://github.com/nvbn/thefuck/issues/1560) — Homebrew Python now requires libexpat to be explicitly linked.

**Solution:** `brew install libexpat` then reinstall thefuck.

### Error: `[self-tests] fail against pytest 9`

**Cause:** Issue [#1549](https://github.com/nvbn/thefuck/issues/1549) — only relevant if you are running `pytest` against The Fuck's own test suite. Does not affect end-user behaviour.

**Solution:** Pin `pytest<9` in your dev environment.

---

## 8. FAQ

**Q: Does The Fuck phone home or upload anything?**

No. Everything runs locally. The only network calls come from rules that explicitly need them (e.g., `apt_get` consults the local `CommandNotFound` database; `pacman_not_found` calls local `pkgfile`). Nothing is sent to a remote service.

**Q: What gets cached and where?**

The `Cache` class in `thefuck/utils.py` writes a `shelve` database under `$XDG_CACHE_HOME/thefuck` (default `~/.cache/thefuck`). It memoises results of expensive commands like `brew --prefix`, `docker --help`, `gem help commands`, etc., keyed by the modification time of the binary so cache invalidates automatically when you upgrade. Delete the directory to reset.

**Q: Can I see what The Fuck would do without running anything?**

Run `thefuck "<the failing command>"` directly. It prints the would-be-evaluated script to stdout but does not eval it. The alias is what actually runs `eval`.

**Q: How do I disable confirmation only for non-destructive rules?**

Not directly — `require_confirmation` is global. The workaround is to keep `require_confirmation=True` and use `-y` selectively (`fuck -y` for one-offs).

**Q: Does The Fuck work over SSH?**

Yes, as long as you install it on the remote machine and add the alias to that machine's shell rc. There is no client/server protocol.

**Q: Does The Fuck work in `tmux`/`screen`?**

Yes. Shell detection walks the parent process tree, so `bash` inside `tmux` resolves correctly. If detection lands on `Generic`, set `TF_SHELL` explicitly.

**Q: Why does `git push` sometimes get fixed twice in a row?**

Because the first fix added `--set-upstream` (rule `git_push`) and the second added `--force-with-lease` (rule `git_push_force`, which is **disabled by default** — but if you opted into it via `rules = [...]`, it can chain). Disable one of them or use `priority` to choose which fires first.

**Q: Why does my shell startup feel slower after adding the alias?**

The alias itself is just a function definition — its cost is negligible. The first call to `fuck` is slower because The Fuck imports every rule file in the catalog. To shave time, set `rules = [...]` in `settings.py` to whitelist just the rules you actually use.

**Q: Why does The Fuck want `script(1)` for instant mode?**

Instant mode replaces your interactive shell with a `script -f` recording so it can read command output from the log file instead of re-running. `script` is part of `bsdutils` on Debian/Ubuntu, `util-linux` on Arch, and ships with macOS. If `script` is missing, instant mode silently falls back to rerun mode.

**Q: Can I run The Fuck inside a non-interactive script?**

Yes, via the raw `thefuck` binary plus `THEFUCK_REQUIRE_CONFIRMATION=false`. But you'll lose the alias's history capture, so pass the failing command as positional args:
```bash
THEFUCK_REQUIRE_CONFIRMATION=false thefuck git pus origin main
```

**Q: Does The Fuck support PowerShell on Linux?**

Yes — both `powershell` and `pwsh` map to the `Powershell` shell class. The alias uses `Get-History -Count 1` for history capture. PowerShell does not support history rewriting (alter_history is a no-op there).

**Q: Where is the source for the rule plugin protocol?**

`thefuck/types.py` defines the `Rule` dataclass and the `from_path` classmethod that loads each rule file. `thefuck/corrector.py` is the orchestrator that walks rule paths, filters by `is_enabled`, and yields suggestions in priority order. See the **Admin Guide** for a deep dive.

**Q: Is there a Windows native build?**

`pip install thefuck` works on Windows. The setup ships `scripts/fuck.bat` and `scripts/fuck.ps1`. Bash/Zsh/Fish are not supported on plain Windows; use PowerShell or WSL.

**Q: How do I share rules across a team?**

Two options:
1. Publish them as a `thefuck_contrib_yourteam` pip package — every member's The Fuck auto-discovers it.
2. Check the rule files into a shared repo and have everyone symlink them into `~/.config/thefuck/rules/`.

**Q: Can I disable colors permanently?**

Yes — `no_colors = True` in `settings.py`, or `THEFUCK_NO_COLORS=true`, or run inside any non-TTY environment (colorama detects this for free).

**Q: Why did the alias change my `PYTHONIOENCODING`?**

The alias temporarily exports `PYTHONIOENCODING=utf-8` so The Fuck can decode shell output safely on Windows. It saves the original value in `TF_PYTHONIOENCODING` and restores it after the call. If you see Unicode errors, your shell may be running an outdated alias — re-eval `$(thefuck --alias)`.

**Q: How does Git alias expansion work?**

Some Git aliases hide the real command (`alias.s = status`). The Fuck sets `GIT_TRACE=1` during the rerun. Git emits `trace: alias expansion: s => status` to stderr; the `@git_support` decorator parses that line and rewrites the script before calling the rule's `match()`. Without `GIT_TRACE`, Git rules cannot see what the alias resolves to.

**Q: Will The Fuck modify my files?**

Only via rules that declare a `side_effect` callable. The bundled side-effect-bearing rules are:
- `dirty_untar` — removes loose extracted files in CWD.
- `dirty_unzip` — removes loose extracted files in CWD.
- `ssh_known_hosts` — deletes the offending line(s) from the named known_hosts file.

These are marked with `(+side effect)` in the prompt — accept only if you understand what they do.

**Q: I heard the project is unmaintained?**

Maintenance has slowed (issues [#1466](https://github.com/nvbn/thefuck/issues/1466) and [#1566](https://github.com/nvbn/thefuck/issues/1566) discuss this). The codebase is stable and the rule mechanism is extensible enough that you can ship your own rules locally without waiting on upstream.

---

## 9. Reference

### Environment variables

| Variable | Maps to setting | Type | Notes |
|---|---|---|---|
| `THEFUCK_RULES` | `rules` | colon-separated | Use `DEFAULT_RULES` to mean "everything" |
| `THEFUCK_EXCLUDE_RULES` | `exclude_rules` | colon-separated | Disabled rules are skipped at import time |
| `THEFUCK_WAIT_COMMAND` | `wait_command` | int seconds | Default 3 |
| `THEFUCK_REQUIRE_CONFIRMATION` | `require_confirmation` | `true`/`false` | Default true |
| `THEFUCK_NO_COLORS` | `no_colors` | `true`/`false` | Default false |
| `THEFUCK_DEBUG` | `debug` | `true`/`false` | Default false |
| `THEFUCK_PRIORITY` | `priority` | colon-separated `key=int` pairs | e.g., `git_push=100:history=9999` |
| `THEFUCK_HISTORY_LIMIT` | `history_limit` | int | Default unlimited |
| `THEFUCK_ALTER_HISTORY` | `alter_history` | `true`/`false` | Default true |
| `THEFUCK_WAIT_SLOW_COMMAND` | `wait_slow_command` | int seconds | Default 15 |
| `THEFUCK_SLOW_COMMANDS` | `slow_commands` | colon-separated | Default `lein:react-native:gradle:./gradlew:vagrant` |
| `THEFUCK_REPEAT` | `repeat` | `true`/`false` | Default false |
| `THEFUCK_INSTANT_MODE` | `instant_mode` | `true`/`false` | Default false |
| `THEFUCK_NUM_CLOSE_MATCHES` | `num_close_matches` | int | Default 3 |
| `THEFUCK_EXCLUDED_SEARCH_PATH_PREFIXES` | `excluded_search_path_prefixes` | colon-separated | Skip these PATH dirs when listing executables |

### Other env vars used internally

| Variable | Set by | Purpose |
|---|---|---|
| `TF_ALIAS` | the shell alias | Lets The Fuck recognise its own alias and skip self-corrections |
| `TF_SHELL` | the shell alias | Forces shell detection bypass |
| `TF_HISTORY` | the shell alias | Pre-captured `fc -ln -10` output passed in |
| `TF_SHELL_ALIASES` | the Bash/Zsh alias | Pre-captured `alias` listing for shell-alias resolution |
| `TF_OVERRIDDEN_ALIASES` / `THEFUCK_OVERRIDDEN_ALIASES` | user | Comma-separated list of Fish aliases that override built-ins (default: `cd, grep, ls, man, open`) |
| `TF_PYTHONIOENCODING` | the shell alias | Saved `$PYTHONIOENCODING`; restored after the call |
| `THEFUCK_OUTPUT_LOG` | instant-mode alias | Path to the shell-logger temp file |
| `XDG_CONFIG_HOME` | OS / user | Base for the settings dir (default `~/.config`) |
| `XDG_CACHE_HOME` | OS / user | Base for the cache dir (default `~/.cache`) |
| `HISTFILE` | shell | Used by Bash/Tcsh/Zsh history reading |
| `EDITOR` | user | Required for the `fix_file` rule to fire |
| `SHELL` | OS | Used by `--shell-logger` |
| `PYTHONIOENCODING` | the shell alias | Forced to `utf-8` for the duration of the call |
| `SHELL_LOGGER_SOCKET` | external integration | If set, The Fuck reads command output via this Unix socket instead of rerunning |

### Default settings

```python
DEFAULT_SETTINGS = {
    'rules': [ALL_ENABLED],
    'exclude_rules': [],
    'wait_command': 3,
    'require_confirmation': True,
    'no_colors': False,
    'debug': False,
    'priority': {},
    'history_limit': None,
    'alter_history': True,
    'wait_slow_command': 15,
    'slow_commands': ['lein', 'react-native', 'gradle', './gradlew', 'vagrant'],
    'repeat': False,
    'instant_mode': False,
    'num_close_matches': 3,
    'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'},
    'excluded_search_path_prefixes': [],
}
```

### Files & directories

| Path | Purpose |
|---|---|
| `~/.config/thefuck/settings.py` | User Python settings (auto-generated) |
| `~/.config/thefuck/rules/` | User custom rules (`.py` files) |
| `~/.thefuck/` | Legacy path (still honoured with a deprecation warning) |
| `$XDG_CACHE_HOME/thefuck` | shelve cache for memoised commands |
| `/tmp/thefuck-script-log-<uuid>` | Instant-mode output log |
| `/tmp/thefuck.last_not_configured_run_<user>` | Tracker for the auto-configure-on-second-run flow |

### Exit codes

| Code | Meaning |
|---|---|
| 0 | A correction was selected and emitted to stdout (the alias `eval`s it). |
| 1 | Nothing matched, or the user pressed Ctrl+C / `q`. |
| -1 | Python version check failed in `setup.py` (install-time only). |

### Related links

- Source: [github.com/nvbn/thefuck](https://github.com/nvbn/thefuck)
- Issues: [github.com/nvbn/thefuck/issues](https://github.com/nvbn/thefuck/issues)
- Latest release: [pypi.org/project/thefuck](https://pypi.org/project/thefuck/)
- Author: Vladimir Iakovlev (`[email removed]`)
- License: MIT

For architecture, the rule plugin protocol, internals, and contributing, continue to the **Admin 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. Unofficial; not affiliated with or reviewed by the project._
