External Files & Archives

Lesson 9 · chezmoi · ~10 minutes

Not everything in your home directory comes from files you wrote. Fonts, shell plugins, binary tools — these live on the internet and you need them pulled into the right place on every machine. Chezmoi's .chezmoiexternal.toml handles this without cluttering your dotfiles repo with third-party code.

The Mental Model

Instead of committing external files into your source state (bloating your repo), you declare where to fetch them from and where to place them. Chezmoi downloads and extracts them at apply time.

How external files flow into your home directory
Remote URL
GitHub release, archive, raw file
.chezmoiexternal.toml
Declares URL + target path
Target State
Files appear in ~/

The declaration lives in your source state root (alongside dot_zshrc and friends). It's a TOML file where each section maps a target path to a remote URL.

type: archive vs type: file

Every entry in .chezmoiexternal.toml needs a type. The two main options:

TypeUse whenWhat happens
archiveURL points to a .tar.gz, .zip, or similarDownloads and extracts into the target directory
fileURL points to a single raw fileDownloads and places the file at the target path

Single file example: a font

Download a single font file from a URL and place it in your fonts directory:

[".local/share/fonts/JetBrainsMono-Regular.ttf"]
    type = "file"
    url = "https://github.com/JetBrains/JetBrainsMono/raw/master/fonts/ttf/JetBrainsMono-Regular.ttf"

The key (in brackets) is the path relative to your home directory where the file will land.

Archive example: oh-my-zsh

Pull the entire oh-my-zsh framework from its GitHub archive:

[".oh-my-zsh"]
    type = "archive"
    url = "https://github.com/ohmyzsh/ohmyzsh/archive/master.tar.gz"
    exact = true
    stripComponents = 1
    refreshPeriod = "168h"

Let's unpack those options:

Key options explained

stripComponents

GitHub archives wrap everything in a top-level directory (like ohmyzsh-master/). Setting stripComponents = 1 removes that wrapper so the contents land directly in the target path. This is the same concept as tar --strip-components=1.

exact

When exact = true, chezmoi ensures the target directory contains only what's in the archive. Any files that exist in the target but aren't in the archive get deleted. Without this, old files from previous versions would accumulate.

Use exact carefully

Only set exact = true when chezmoi should own the entire directory. If other tools write into that same directory, those files will be deleted on the next chezmoi apply.

refreshPeriod

By default, chezmoi re-downloads external files on every apply. That's slow and unnecessary for things that rarely change. Set refreshPeriod to cache:

refreshPeriod = "168h"   # Re-download after 7 days
refreshPeriod = "720h"   # Re-download after 30 days

Chezmoi stores a cache of downloaded externals. If the refresh period hasn't elapsed, it skips the download entirely. You can force a re-download with:

chezmoi apply --refresh-externals
Choosing a refresh period

For plugins that track master: 7 days (168h) is reasonable. For pinned releases: 30 days or more. For font files that never change: omit refreshPeriod and they'll only download once.

Real-world patterns

Zsh plugin from a GitHub archive

[".oh-my-zsh/custom/plugins/zsh-autosuggestions"]
    type = "archive"
    url = "https://github.com/zsh-users/zsh-autosuggestions/archive/master.tar.gz"
    exact = true
    stripComponents = 1
    refreshPeriod = "168h"

Binary tool from a GitHub release

[".local/bin/fzf"]
    type = "archive-file"
    url = "https://github.com/junegunn/fzf/releases/download/v0.55.0/fzf-0.55.0-darwin_arm64.tar.gz"
    path = "fzf"
    refreshPeriod = "720h"

The archive-file type extracts a single file from within an archive. The path key specifies which file inside the archive to extract.

Multiple plugins together

[".oh-my-zsh/custom/plugins/zsh-syntax-highlighting"]
    type = "archive"
    url = "https://github.com/zsh-users/zsh-syntax-highlighting/archive/master.tar.gz"
    exact = true
    stripComponents = 1
    refreshPeriod = "168h"

[".oh-my-zsh/custom/plugins/zsh-completions"]
    type = "archive"
    url = "https://github.com/zsh-users/zsh-completions/archive/master.tar.gz"
    exact = true
    stripComponents = 1
    refreshPeriod = "168h"

Each plugin gets its own section. The pattern is identical — just change the URL and target path.

Exercise: Add zsh-autosuggestions

Step 1: Create the external file

Open (or create) .chezmoiexternal.toml in your source state root:

chezmoi edit ~/.chezmoiexternal.toml

If the file doesn't exist yet, chezmoi will create it. Alternatively, create it directly:

chezmoi cd
touch .chezmoiexternal.toml

Step 2: Add the plugin declaration

Add this to .chezmoiexternal.toml:

[".oh-my-zsh/custom/plugins/zsh-autosuggestions"]
    type = "archive"
    url = "https://github.com/zsh-users/zsh-autosuggestions/archive/master.tar.gz"
    exact = true
    stripComponents = 1
    refreshPeriod = "168h"

Step 3: Apply and verify

chezmoi apply

Check that the plugin landed in the right place:

ls ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions/

You should see files like zsh-autosuggestions.zsh, README.md, etc.

Step 4: Commit

chezmoi cd
git add .chezmoiexternal.toml
git commit -m "chore: add zsh-autosuggestions via external archive"
git push
exit

Now any machine that runs chezmoi apply will automatically pull down zsh-autosuggestions without you needing to clone it manually.

What just happened

ConceptWhat it does
.chezmoiexternal.tomlDeclares remote files/archives to pull at apply time
type = "file"Downloads a single file to the target path
type = "archive"Downloads and extracts an archive into a directory
stripComponentsRemoves N leading path segments from archive entries
exact = trueEnsures target directory only contains archive contents
refreshPeriodCaches downloads to avoid re-fetching on every apply
--refresh-externalsForces re-download regardless of cache

Knowledge check

What does stripComponents = 1 do when extracting a GitHub archive?
Correct. GitHub archives always wrap contents in a directory like repo-master/. Setting stripComponents = 1 removes that layer so files land directly in your target path.
Not quite. stripComponents removes leading directory levels from archive paths — just like tar --strip-components. GitHub archives wrap everything in repo-branch/, so stripping 1 level puts files directly in the target.
When should you use type = "file" instead of type = "archive"?
Right. Use file when the URL is a direct link to a single file — a font, a script, a binary. Use archive when it's a compressed bundle that needs extracting.
Use type = "file" when the URL points directly to a single file (not compressed). For .tar.gz, .zip, or other archives, use type = "archive".
What happens if you set exact = true on an external archive and a file exists in the target directory that isn't in the archive?
Exactly. exact = true means chezmoi owns that directory completely. Anything not in the archive gets removed. This keeps plugin directories clean across updates.
With exact = true, chezmoi ensures the directory matches the archive exactly — extra files get deleted. This is intentional for directories chezmoi should fully own.

Next up

You can now pull external dependencies declaratively — no manual git clone or curl scripts needed. In Lesson 10: Bootstrap a New Machine, you'll tie everything together: init, templates, scripts, and externals into a single command that sets up a fresh machine from scratch.

Recommended Reading

chezmoi: Include files from elsewhere — The full reference for .chezmoiexternal.toml including all supported types, options, and advanced patterns like filters and checksums.

Questions? Ask me anything that's unclear. I can explain the difference between archive types, help you figure out the right stripComponents value, or troubleshoot downloads that aren't landing where expected.
← Prev Next →