Skip to main content

Declarative dotfile, shell, and environment configuration management.

Project description

prescribe

Declarative, idempotent config management with rollback, diff preview, and OS-level environment persistence.

Write a TOML spec describing what your config files, env vars, and shell blocks should look like. Run prescribe apply. Done. prescribe status --diff shows exactly what would change before you commit.

pip install prescribe

Commands

prescribe apply spec.toml              # Apply all changes
prescribe apply spec.toml --dry-run    # Preview without writing
prescribe apply spec.toml --tags agent # Only targets tagged "agent"
prescribe status spec.toml             # Show sync status
prescribe status --diff spec.toml      # Show unified diffs of what would change
prescribe list-specs specs             # List top-level specs in sorted apply order
prescribe apply-dir specs              # Apply every top-level *.toml spec in a directory
prescribe status-dir specs --diff      # Preview a whole spec directory
prescribe validate-dir specs --plan    # Validate and plan a whole spec directory
prescribe list                         # List all managed files
prescribe rollback path/to/file        # Roll back changes to a file

All commands accept --json for machine-readable output and --state (or PRESCRIBE_STATE env var) to set the database path.

prescribe rollback accepts --on-conflict (prompt/revert/ignore) and --original (restore pre-prescribe state). For mirrored assets with replace = true, rolling back the mirror destination directory restores displaced extra files from managed backup storage.

Mutating commands use a short-lived SQLite run lock to prevent concurrent writes. Prescribe also records durable ownership claims for managed keys, blocks, env vars, and assets; overlapping claims fail before writes unless ownership is explicitly taken with --on-claim-conflict take or approved interactively with --on-claim-conflict prompt.

By default, prescribe refuses to place its SQLite state database on a network filesystem such as UNC, mapped network drives, SMB, SSHFS, or NFS. Keep state on a local disk for reliable locking. If you intentionally accept the risk, set PRESCRIBE_ALLOW_NETWORK_STATE=1 or pass --allow-network-state to mutating commands.


Spec format

A spec has an optional [vars] section and four optional target sections: [[files]], [[assets]], [[env]], and [[shell]].

[vars] — reusable variables

Define once, use anywhere in env values and paths.

[vars]
LOCAL_BIN = "$HOME/.local/bin"
TOOLS_BIN = "~/tools/bin"

[locations] — reusable path resolution

Use named locations when several targets need the same fallback path logic.

[locations.codex_home]
kind = "dir"
mode = "first_existing_parent"
candidates = [
  { path = "$USERPROFILE/.codex", platforms = ["windows"] },
  { path = "~/.codex", platforms = ["linux", "macos", "wsl"] },
]

[locations.cursor_mcp]
kind = "file"
candidates = [
  { path = "$APPDATA/Cursor/User/mcp.json", platforms = ["windows"] },
  { path = "~/Library/Application Support/Cursor/User/mcp.json", platforms = ["macos"] },
  { path = "~/.config/Cursor/User/mcp.json", platforms = ["linux", "wsl"] },
]
  • candidates — non-empty list of paths, or tables with path, platforms, machine, and if_command_exists
  • modefirst_existing_parent (default), first_existing, required, first, or create_parent
  • kindfile, dir, or any; existing candidates with the wrong kind are rejected

Targets can reference a location instead of repeating paths:

[[files]]
location = "cursor_mcp"
format = "jsonc"

[files.data]
"mcpServers.local.command" = "uvx"

[[assets]]
source = "codex/skills/**/*.md"
dest_location = "codex_home"
dest_append = "skills"
mode = "mirror"
replace = true

[[files]] — config file management

[[files]]
path = "~/.gitconfig"
format = "toml"
tags = ["base", "git"]

[files.data]
"core.editor" = "nvim"
"core.autocrlf" = "input"
"init.defaultBranch" = "main"

[[files]]
path = "~/.gitconfig"
format = "toml"
priority = 10
tags = ["work"]

[files.data]
"user.name" = "Work Name"
"user.email" = "work@example.com"
  • priority — lower wins when multiple entries target the same file (default 0)
  • delete — list of dotted keys to remove
  • formattoml, yaml, jsonc, or line
  • location / path_append — use a named [locations] entry, optionally with a relative child path
  • paths — fallback list if path doesn't exist
  • text / text_from — for format = "line" managed blocks, define block content inline or from a file

[[assets]] — repo-owned file materialization

Use assets when your dotfiles repo should own a whole file or tree at the destination. Unlike [[files]], an asset replaces the destination file with the source content. Prescribe still records checkpoints, detects external edits on later applies, and can roll back created or overwritten files.

[[assets]]
source = "codex/mcp.json"
dest = "~/.codex/mcp.json"
tags = ["agent"]

[[assets]]
source = "codex/skills/**/*.md"
dest = "~/.codex/skills"
mode = "mirror"
replace = true
tags = ["agent"]
  • source — file path or glob pattern, relative to the spec file unless absolute
  • dest — destination file for mode = "file" or destination directory for mode = "mirror"
  • dest_location / dest_append — use a named [locations] entry, optionally with a relative child path
  • paths — fallback destination list; the first existing path wins, then first existing parent
  • modefile or mirror; glob sources default to mirror, otherwise file
  • delete_extra — reserved for future mirror pruning; currently must stay false
  • replace — for mode = "mirror", move extra destination files into Prescribe backup storage instead of leaving them active
  • max_displace_bytes — maximum size of an extra file that replace may move; defaults to 10 MiB
  • allow_binary — allow replace to move binary-looking extra files; defaults to false

Asset destinations are replaced at the path itself. If the destination is a symlink or hardlink, Prescribe avoids mutating the linked target content; rollback restores previous file content and restores symlink destinations when possible. When replace = true, Prescribe refuses broad destinations such as the home directory or filesystem roots, refuses directories, and moves extra files/symlinks to managed backup storage before recording a restore manifest. To recover displaced extras from a mirror destination, roll back the mirror directory:

prescribe rollback ~/.codex/skills

For shell snippets or other file fragments that should not own the whole file, use format = "line" managed blocks instead:

[[files]]
path = "~/.config/powershell/Microsoft.PowerShell_profile.ps1"
format = "line"
managed_block_id = "aliases"
text = """
Set-Alias ll Get-ChildItem
function gs { git status @args }
"""

[[files]]
path = "~/.ssh/config"
format = "line"
managed_block_id = "work-hosts"
text_from = "blocks/ssh-work-hosts.txt"

[[env]] — environment variables

[[env]]
name = "EDITOR"
value = "nvim"
materialize = true          # persist to OS (macOS launchd, Linux systemd, Windows registry)

[[env]]
name = "CARGO_HOME"
value = "~/.cargo"
path_prepend = ["$CARGO_HOME/bin"]   # set the var AND add its bin to PATH

[[env]]
name = "PATH"
prepend = ["$LOCAL_BIN"]
append = ["/usr/local/sbin"]

[[env]]
name = "SECRET_TOKEN"
value = "!fnox get SECRET_TOKEN"     # !command values kept as literal strings
tags = ["secrets"]
  • value — last one wins for duplicate names
  • prepend / append — accumulated in spec order, deduplicated
  • path_prepend / path_append — forwarded to PATH construction
  • materialize — persist to OS-level store (best-effort, no secrets check)
  • !command values pass through as literal strings (shell block resolves at source time)
  • if_env_missing = true — skip if already set in process env

[[shell]] — managed block injection

[[shell]]
path = "~/.xonshrc"
managed_block_id = "prescribe-env"
shells = ["xonsh"]

Prescribe writes a managed block with env var exports. Supported shells: xonsh, bash, zsh, fish, nu, pwsh, and cmd. The block is idempotent — re-applying replaces the entire block atomically. Existing content outside the block is never touched.

Conditions

Targets and location candidates support gating:

Field Type Behaviour
platforms list[str] One of linux, macos, windows, wsl. Empty = all.
arch list[str] One of x86_64, arm64, x86, armv7, all. Empty = all.
machine list[str] Hostname match or PRESCRIBE_MACHINE override.
tags list[str] Runtime filter via --tags / --skip-tags. Empty = always.
if_command_exists list[str] Skip if the binary isn't installed.
shells list[str] For [[env]] and [[shell]]: one of xonsh, bash, zsh, fish, nu, pwsh, cmd.

Rollback and conflict detection

Prescribe tracks every change in a SQLite state database. Managed keys are fingerprinted before and after every apply. If someone edits a managed key outside of prescribe, the next apply detects the conflict and refuses to overwrite.

prescribe rollback ~/.gitconfig                    # Roll back last change
prescribe rollback ~/.gitconfig --original         # Restore to pre-prescribe state
prescribe rollback ~/.gitconfig --on-conflict revert  # Force-revert external edits

Library API

import prescribe

# Parse a spec
spec = prescribe.SpecLoader().load(Path("spec.toml"))

# Or build one programmatically
spec = prescribe.Spec(
    id="my-spec",
    files=[prescribe.FileTarget(
        path=Path("~/.gitconfig"),
        format="toml",
        data={"core.editor": "nvim"},
    )],
    env=[prescribe.EnvTarget(
        name="EDITOR",
        value="nvim",
    )],
)

# Apply
store = prescribe.StateStore("~/.prescribe/state.db")
store.initialize()
orch = prescribe.Orchestrator(state_store=store)

results = orch.run(spec=spec)                      # Apply
results = orch.run(spec=spec, dry_run=True)        # Status check
results = orch.run(spec=spec, tags={"agent"})      # Filter by tags
results = orch.run(spec=spec, diff=True)           # Include diffs

for r in results:
    print(f"{r.status:12} {r.diff or ''}")

# Resolved env vars
print(results[0].env_vars)  # {"EDITOR": "nvim", "PATH": "..."}

# Rollback
orch.rollback(Path("~/.gitconfig"))

# Conditions as standalone functions
from prescribe import platform_matches, machine_matches, condition_matches

# Shell block rendering
from prescribe import render_shell_block
render_shell_block(shell_type="xonsh", env_vars={"EDITOR": "nvim"}, managed_block_id="prescribe-env")

# Read existing shell files without applying or mutating anything
from prescribe import extract_shell_env_file
extraction = extract_shell_env_file(Path("~/.zshrc").expanduser())
print(extraction.updates)
print(extraction.issues)

# Inspect intentionally installed tools and manager-owned executable paths
from prescribe import inspect_installed_tools, inspect_tool_paths

installed = inspect_installed_tools(managers=["uv", "mise", "scoop"])
print(installed.installed)

paths = inspect_tool_paths(["uv", "ruff"], managers=["uv", "mise", "scoop"])
print(paths.tools)      # {"uv": (...), "ruff": (...)}
print(paths.sources)    # manager-owned search roots
print(paths.issues)     # non-fatal inspection issues

# Materialize env vars to OS
from prescribe import materialize
materialize(env_vars={"EDITOR": "nvim"})

Principles

  • Declarative — spec says what the state should be, not how to get there
  • Idempotent — re-running produce zero changes if everything is in sync
  • Rollback — every change is recorded; undo to any previous state
  • Round-trip safe — unmanaged keys, comments, and formatting are preserved
  • Cross-platform — macOS, Linux, Windows. File paths and env persistence work everywhere
  • Pure function design — conditions and shell rendering are standalone functions, no hidden state

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

prescribe-0.3.1.tar.gz (80.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

prescribe-0.3.1-py3-none-any.whl (100.0 kB view details)

Uploaded Python 3

File details

Details for the file prescribe-0.3.1.tar.gz.

File metadata

  • Download URL: prescribe-0.3.1.tar.gz
  • Upload date:
  • Size: 80.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for prescribe-0.3.1.tar.gz
Algorithm Hash digest
SHA256 684c8546ddce5b24f6eae4a58ed2431997d9d26474e5043e609f3ca8adf3a0fa
MD5 e959996f8788c6270d28106f5fbdc43c
BLAKE2b-256 b22fab1dedb2a42c3282a77d0cee849594e98d1433f44cdf5dae468fbca6aa1c

See more details on using hashes here.

File details

Details for the file prescribe-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: prescribe-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 100.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for prescribe-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e48e7fe37b3b47a17f42f45780cc5044225704772586f412cdad71d4f362c4eb
MD5 d63bd2f3418008acf562104597a813fe
BLAKE2b-256 8cfe1185f067ccdb988806e238ce60346cbb2a310395e7540aac53f9e278ee8a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page