# Create a CLI extension

The `hf` CLI supports extensions, custom commands provided by the community that integrate seamlessly
into the CLI. Extensions are hosted as public GitHub repositories and can be installed with a single command.
Once installed, they appear as top-level `hf` commands just like built-in ones.

This system is inspired by [GitHub CLI extensions](https://docs.github.com/en/github-cli/github-cli/creating-github-cli-extensions).
In this guide, you will learn how to create your own extension, publish it, and make it discoverable.

> [!TIP]
> For user-facing documentation on installing and managing extensions, see the
> [CLI reference for `hf extensions`](../package_reference/cli#hf-extensions).

## Overview

There are two types of extensions:

1. **Shell script extensions**: a single executable file (bash script, compiled binary, etc.) placed at the
   root of the repository.
2. **Python extensions**: a standard Python package with a `pyproject.toml`. Installed in an isolated virtual
   environment so dependencies don't conflict with the user's system.

Both types share the same conventions:

- The GitHub repository **must** be named `hf-<name>` (e.g., `hf-claude`, `hf-mem`).
- Once installed, users run the extension with `hf <name>` (e.g., `hf claude`).
- The extension is listed in `hf --help` under "Extension commands".

When a user runs `hf extensions install [OWNER/]hf-<name>`, the system first looks for a binary/script file
named `hf-<name>` at the repository root. If found, it installs as a shell script extension. Otherwise, it
falls back to installing the repo as a Python package.

## Create a shell script extension

A shell script extension is the simplest type. You only need a GitHub repository with an executable file
named `hf-<name>` at the root.

> [!WARNING]
> Shell script extensions are not supported on Windows. If your extension must work on Windows, make it a
> [Python extension](#create-a-python-extension) instead.

### Minimal example

Create a repository named `hf-hello` on GitHub with a single file:

**`hf-hello`** (at the repository root):

```bash
#!/usr/bin/env bash
set -euo pipefail

echo "Hello from hf-hello extension!"
echo "Arguments: $@"
```

That's it! Users can now install and run it:

```bash
>>> hf extensions install <your-username>/hf-hello
>>> hf hello
Hello from hf-hello extension!
Arguments:
```

### Tips for shell script extensions

- Always start with a shebang (`#!/usr/bin/env bash`) and `set -euo pipefail` for safety.
- The script receives all extra arguments passed by the user. For example, `hf hello --name world`
  passes `--name world` to the script.
- You can access the user's Hugging Face token via the `HF_TOKEN` environment variable if they are logged in.
- Add a `manifest.json` at the repository root to provide a description (see [Add a description](#add-a-description)).
- External dependencies (e.g., `fzf`, `jq`, etc.) are **not** installed automatically with your extension. Check
  for required tools at the start of your script and fail gracefully with a helpful error message if they are missing.

> [!TIP]
> For a real-world example, see [hanouticelina/hf-claude](https://github.com/hanouticelina/hf-claude) —
> a shell script extension that launches Claude Code with HF Inference Providers.

## Create a Python extension

Python extensions are full Python packages installed in an isolated virtual environment. This is the best
choice when your extension has Python dependencies or more complex logic.

### Minimal example

Create a repository named `hf-hello` on GitHub with this structure:

```
hf-hello/
├── pyproject.toml
└── src/
    └── hf_hello/
        ├── __init__.py
        └── cli.py
```

**`pyproject.toml`**:

```toml
[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.backends._legacy:_Backend"

[project]
name = "hf-hello"
version = "0.1.0"
description = "A hello-world hf CLI extension"
requires-python = ">=3.10"

[project.scripts]
hf-hello = "hf_hello.cli:main"
```

**`src/hf_hello/cli.py`**:

```python
import sys

def main():
    print("Hello from hf-hello extension!")
    print(f"Arguments: {sys.argv[1:]}")

if __name__ == "__main__":
    main()
```

The critical part is the `[project.scripts]` entry point: it **must** be named `hf-<name>` (here, `hf-hello`).
This is how the `hf` CLI discovers and executes your extension.

Users install and run it the same way:

```bash
>>> hf extensions install <your-username>/hf-hello
>>> hf hello
Hello from hf-hello extension!
Arguments: []
```

### How Python extensions are installed

When a user installs a Python extension, the following happens:

1. A virtual environment is created at `~/.local/share/hf/extensions/hf-<name>/venv/`.
2. Your package is installed via `pip install` from the GitHub repo archive.
3. The system verifies that a `hf-<name>` console script was created in the venv.

This means your extension's dependencies are fully isolated — they won't conflict with the user's
other Python packages.

> [!TIP]
> For a real-world example, see [alvarobartt/hf-mem](https://github.com/alvarobartt/hf-mem) —
> a Python extension that estimates inference memory requirements for HF models.

## Add a description

A description helps users understand what your extension does. It appears in `hf extensions list`
and in `hf --help`.

The system looks for a description in the following order:

1. **`manifest.json`** at the repository root:

```json
{
    "description": "A short description of what your extension does"
}
```

2. **`pyproject.toml`** `description` field (for Python extensions):

```toml
[project]
description = "A short description of what your extension does"
```

3. **GitHub repository description** (the "About" field on the repo page).

For Python extensions, setting `description` in `pyproject.toml` is the most natural approach.
For shell script extensions, use a `manifest.json` file or set the GitHub repository description.

## Make your extension discoverable

To help users find your extension, add the **`hf-extension`** topic to your GitHub repository:

1. Go to your repository on GitHub.
2. Click the gear icon next to "About" on the right sidebar.
3. Under "Topics", add `hf-extension`.

This is a community convention that makes it easy to browse all available extensions on the
[hf-extension topic page](https://github.com/topics/hf-extension).

Users can then discover your extension directly from the CLI with `hf extensions search`, which lists
all GitHub repositories tagged with the `hf-extension` topic, sorted by stars:

```bash
>>> hf extensions search
NAME   REPO                    STARS DESCRIPTION                         INSTALLED
------ ----------------------- ----- ----------------------------------- ---------
claude hanouticelina/hf-claude     2 Extension for `hf` CLI to launch... yes
agents hanouticelina/hf-agents       HF extension to run local coding...
```

The `INSTALLED` column shows which extensions are already installed locally. From there, users can
install any listed extension with `hf extensions install <repo>`.

## Test your extension

During development, you can install your extension directly from your GitHub repository:

```bash
# Install from your repo
>>> hf extensions install <your-username>/hf-<name>

# Run it
>>> hf <name>

# Reinstall after making changes (push to GitHub first)
>>> hf extensions install <your-username>/hf-<name> --force

# Update to the latest version (only if a newer commit is available)
>>> hf extensions update <name>

# List installed extensions
>>> hf extensions list

# Remove when done
>>> hf extensions remove <name>
```

> [!TIP]
> Use `--force` to overwrite a previously installed version when testing updates.

## Update installed extensions

Once an extension is installed, you can update it to the latest version published on GitHub:

```bash
# Update a single extension (accepts `<name>`, `hf-<name>` or `OWNER/hf-<name>`)
>>> hf extensions update hf-claude

# Check every installed extension and update the outdated ones
>>> hf extensions update
```

`hf extensions update` only updates extensions that are already installed. If the extension is not installed, an error is raised. Extensions already up to date are skipped.

An extension's version is the commit it was installed from, shown as a short SHA in the `VERSION` column of `hf extensions list`. The `LATEST` column is checked when no newer commit is available on GitHub, and running an outdated extension prints a hint suggesting `hf extensions update <name>`. To stay within GitHub's rate limits for unauthenticated requests, `hf` looks up the latest commit of each extension at most once a day. Set `HF_HUB_DISABLE_UPDATE_CHECK=1` to disable these checks.

If a Python extension stops working because the Python it was installed with was removed (e.g. after a Homebrew upgrade), `hf extensions update <name>` recreates its virtual environment.

## Naming rules

Extension names must follow these rules:

- The GitHub repository must be named `hf-<name>`.
- `<name>` must start with a letter or digit.
- `<name>` can contain letters, digits, `.`, `_`, and `-`.
- `<name>` cannot conflict with a built-in `hf` command (e.g., `download`, `upload`, `auth`).

When installing, users can either specify the full `OWNER/hf-<name>` or just `hf-<name>` (which
defaults to the `huggingface` organization).

## Existing extensions

Here are some community extensions you can use as reference:

| Extension | Type | Description |
|-----------|------|-------------|
| [hanouticelina/hf-claude](https://github.com/hanouticelina/hf-claude) | Shell script | Launch Claude Code with HF Inference Providers |
| [alvarobartt/hf-mem](https://github.com/alvarobartt/hf-mem) | Python | Estimate inference memory requirements for HF models |

