# How to Write Omarchy Migrations: Rules and Best Practices

> Learn Omarchy migration rules and best practices. Discover how these idempotent shell scripts synchronize user installations after updates with omarchy-migrate.

- Repository: [37signals/omarchy](https://github.com/basecamp/omarchy)
- Tags: best-practices
- Published: 2026-08-27

---

**Omarchy migrations are idempotent shell scripts stored in `migrations/*.sh` that execute per-user via `omarchy-migrate` to synchronize existing installations after updates that pacman cannot handle alone.**

Omarchy migrations provide a mechanism to update user-specific configurations and state when the basecamp/omarchy repository changes. These single-run scripts live in the `migrations/` directory and execute automatically during `omarchy update` or at graphical login, ensuring every user receives necessary filesystem changes regardless of when they originally installed Omarchy.

## What Are Omarchy Migrations?

Omarchy migrations are transformation scripts that bring existing installations into sync after package updates. Unlike database schema migrations, these are shell scripts that modify user configuration files, install optional dependencies, or retire legacy services. According to [`agents/skills/migrations.md`](https://github.com/basecamp/omarchy/blob/main/agents/skills/migrations.md), they run exclusively per-user via the `omarchy-migrate` command and must complete successfully before the user accesses their desktop environment.

## Core Rules for Writing Omarchy Migrations

### File Location and Naming

All migration scripts belong in the `migrations/` directory with a `.sh` extension. Use the helper command `omarchy-dev-add-migration --no-edit` to scaffold new files with correct Unix timestamps (e.g., [`migrations/1785095882.sh`](https://github.com/basecamp/omarchy/blob/main/migrations/1785095882.sh)). This naming convention ensures sequential execution based on creation time.

### Permissions and Execution Context

Migration files must use permissions `0644` (readable, not executable) because the runner invokes them with `bash -euo pipefail`. Do not include a `#!/bin/bash` shebang. The `omarchy-migrate` binary in `bin/omarchy-migrate` executes these scripts as the current Omarchy user, normally during the update process or at graphical login.

### Per-User Completion State

After a migration succeeds, the system creates a marker file at `~/.local/state/omarchy/migrations/<migration filename>`. Each user maintains their own marker directory, guaranteeing that every user account on the system runs the migration exactly once. This design isolates user state while ensuring consistent transformation across multi-user installations.

### Idempotence Requirements

Every migration must detect whether its work has already been performed by another user or previous run and exit harmlessly. This prevents duplicate configuration changes and ensures safe re-runs during testing. Check for existing files, symlinks, or configuration entries before performing destructive operations.

### Major Version Boundaries

Regular migrations handle routine updates within a major version. Major version upgrades (e.g., 3 → 4) are handled by the `omarchy-upgrade-to-quattro` command rather than the standard migration system, as documented in the migration guide.

## Required Migration Structure

### Opening Description

Begin every migration with an `echo` statement that briefly describes the operation. This provides debugging output visible during `omarchy update` execution and creates an audit trail in system logs.

### Path Handling

Use `$OMARCHY_PATH` for repository-relative paths when referencing assets within the Omarchy installation. For user-specific modifications, reference `$HOME` directly. Never hardcode absolute paths that assume specific username or home directory structures.

### Helper Commands

Leverage Omarchy helper utilities for common operations:

- **`omarchy-cmd-present`** — Verify a command exists before using it
- **`omarchy-cmd-missing`** — Check if a binary is absent (returns success if missing)
- **`omarchy-pkg-add`** — Install optional packages only when needed

These wrappers ensure consistent package management and command validation without requiring manual pacman calls.

### Prohibited Actions

Never restart the Omarchy shell within a migration. The `omarchy update` command handles shell lifecycle management automatically after all migrations complete. Restarting prematurely would interrupt the update process and leave the system in an inconsistent state.

## Creating and Testing Migrations

### Scaffolding New Migrations

Generate correctly named migration files using:

```bash
omarchy-dev-add-migration --no-edit

```

This creates `migrations/<unix-timestamp>.sh` with appropriate permissions and places it in the correct directory.

### Testing Locally

Test migrations against a temporary home directory to avoid contaminating your active configuration:

```bash
TMPHOME=$(mktemp -d)
HOME=$TMPHOME bash -euo pipefail migrations/1785095882.sh

```

Alternatively, delete the completion marker to force re-execution on your actual account:

```bash
rm -f "$HOME/.local/state/omarchy/migrations/1785095882.sh"
omarchy-migrate

```

## Code Examples

### Minimal Migration Skeleton

```bash

# migrations/$(date +%s).sh

echo "Link Neovim theme to the current Omarchy theme state"

theme_link="$HOME/.config/nvim/lua/plugins/theme.lua"
target="../../../../.local/state/omarchy/current/theme/neovim.lua"

# Bail out if the symlink already exists or target is missing

[[ -L $theme_link && $(readlink "$theme_link") == "$target" ]] && exit 0

# Create (or replace) the symlink

ln -sfn "$target" "$theme_link"

```

This example demonstrates idempotence by checking for existing symlinks, uses `$HOME` for portability, and starts with a descriptive echo.

### Using Omarchy Helper Commands

```bash
echo "Ensure optional package 'bat' is installed"

# Only install if the command is missing

omarchy-cmd-missing bat && omarchy-pkg-add bat

```

The `omarchy-cmd-missing` helper returns success only when the binary is absent, allowing conditional installation without manual path checks. Note that `set -e` is unnecessary because the migration runner already invokes scripts with `bash -euo pipefail`.

### Real-World Example

The file [`migrations/1785095882.sh`](https://github.com/basecamp/omarchy/blob/main/migrations/1785095882.sh) demonstrates retiring a legacy systemd watcher and installing a new login-time notifier. This migration checks for the presence of the old service, removes it if found, and enables the replacement—exemplifying the idempotent pattern required for safe system updates.

## Summary

- **Location**: Store all migration scripts in `migrations/*.sh` using Unix timestamp filenames
- **Permissions**: Set files to `0644` with no shebang; the runner handles execution via `bash -euo pipefail`
- **State Tracking**: Markers are stored per-user at `~/.local/state/omarchy/migrations/<filename>`
- **Idempotence**: Every script must detect existing work and exit harmlessly to prevent duplicate changes
- **Structure**: Begin with `echo`, use `$OMARCHY_PATH` for repo assets, leverage helper commands like `omarchy-pkg-add`
- **Testing**: Use temporary `$HOME` directories or delete markers to validate changes safely

## Frequently Asked Questions

### Where should I place new Omarchy migration scripts?

Place all migration scripts in the `migrations/` directory at the repository root. Use the `omarchy-dev-add-migration --no-edit` command to generate properly named files using Unix timestamps (e.g., [`migrations/1785095882.sh`](https://github.com/basecamp/omarchy/blob/main/migrations/1785095882.sh)). Manual creation is possible but the helper ensures consistent naming conventions required by `bin/omarchy-migrate`.

### How does Omarchy track which migrations have already executed?

The system creates marker files in `~/.local/state/omarchy/migrations/` using the exact filename of the migration script. Each user account maintains its own markers, ensuring per-user execution. The `omarchy-migrate` runner checks for these markers before executing scripts and creates them immediately after successful completion.

### What does it mean for a migration to be idempotent?

An idempotent migration detects whether its intended changes already exist and exits cleanly without performing redundant work. For example, checking `[[ -L $symlink ]]` before creating a link, or verifying a package is missing before installing it. This prevents duplicate configuration entries and allows safe re-running if a user deletes their marker file.

### How do I test migrations without affecting my live Omarchy installation?

Create a sandboxed environment by setting a temporary `$HOME` variable before executing the script with the same flags used by the runner: `TMPHOME=$(mktemp -d); HOME=$TMPHOME bash -euo pipefail migrations/filename.sh`. This isolates filesystem changes to a disposable directory while validating logic against the actual Omarchy repository structure referenced via `$OMARCHY_PATH`.