GENV

Global Environment Manager ✦ Track, sync, and reproduce your environment

← Back to Home

A thin layer over the package managers you already use. Desired state lives in one git-friendly genv.json. Run genv apply and the machine matches the spec.

macOS

brew tap ks1686/tap && brew install --cask genv

Arch / Manjaro

paru -S genv

Also yay -S genv or the genv-bin package.

Other Linux

curl -Lo genv.tar.gz https://github.com/ks1686/genv/releases/latest/download/genv_4.7.4_linux_amd64.tar.gz tar -xzf genv.tar.gz && sudo mv genv /usr/local/bin/

Windows (PowerShell)

Invoke-WebRequest -Uri https://github.com/ks1686/genv/releases/latest/download/genv_4.7.4_windows_amd64.zip -OutFile genv.zip Expand-Archive genv.zip -DestinationPath .

scoop bucket add ks1686 https://github.com/ks1686/scoop-bucket then scoop install genv (self-hosted bucket). winget and choco installers for the genv binary are unpublished. Once genv is on PATH, it still manages packages through those managers.

Any platform (Go 1.24+)

go install github.com/ks1686/genv@latest

Multi-target

Schema v8 splits shared defaults from OS buckets: macOS, Windows, Arch, Ubuntu-like Linux, and WSL2.

Declarative spec

Commit genv.json. The lock file stays machine-local and is never committed.

Portable mapping

genv map and genv export help move a spec across Linux families without guessing managers.

Version pinning

Pin with constraints like 0.10.*, or omit the version for latest.

Dry-run first

genv apply --dry-run prints the reconcile plan. Add --yes for CI.

PowerShell parity

Schema v7+ can target powershell for aliases, profiles, and hooks on native Windows.

Plugin adapters

Schema v8 adapters cover plugin CLIs genv does not ship built-in (claude plugin, gh extension, …). Set prefer to the adapter name.

Managed externals

Schema v9 can attach a managed external recipe that discovers GitHub Releases or structured HTTP releases, verifies the artifact, and installs or removes it.

Local modules

Schema v10 registers local JSON modules and each target picks them with useModules. Conflicting declarations are an error naming both origins, never a silent override.

Verified services

Schema v10 services declare requires, watch, restart_policy, and health_check; genv upgrade restarts only on a proven change. Windows gets a Task Scheduler backend via scheduled_task.

Target Detected when Typical managers
macosmacOSbrew mas
windowsnative Windowswinget scoop choco
archnative Arch / Arch-likepacman paru yay
ubuntuUbuntu-like Linux or Ubuntu-like WSL2apt snap linuxbrew
wsl-archArch-like WSL2pacman paru yay
linuxoptional catch-all via --target / $GENV_TARGETapt dnf apk …

WSL2 does not inherit native arch. Put shared bits in defaults; use targets.ubuntu or targets.wsl-arch for distro-specific packages.

# One machine
genv init
genv add git
genv add neovim --version "0.10.*"
genv scan
genv status
genv apply --dry-run
genv apply --yes

# Multiple machines (schema v8)
genv migrate --write
genv map --target arch
genv export --target ubuntu --out ./u
genv apply --target ubuntu --dry-run
genv apply --target ubuntu --yes

Active target resolution: --target β†’ $GENV_TARGET β†’ host classification. Spec default is ~/.config/genv/genv.json; the lock is machine-local.

CommandPurpose
add / removeTrack + install / untrack + uninstall
adopt / disownTrack without install / untrack without uninstall
scan / status / listBulk-adopt, show drift, show lock
applyReconcile (--dry-run, --yes, --json, --target, --force-new-lock)
migrateUpgrade a v1-v7 spec to v8 targets (v10 specs are left as-is)
export / mapSingle-target snapshot / assist-only manager suggestions
updates / profile / pullBackground checker, named overlays, fetch spec from repo
env / shell / serviceEnv vars, aliases, user services (service restart <name>)
config / explainComposed v10 environment (--registry) / where one resource comes from
completionbash, zsh, fish, powershell
  • --file <path>: spec path (default under ~/.config/genv/)
  • --lock-file <path>: lock path (default genv.lock.json)
  • --target <id>: v8 target for apply / status / upgrade / mutate / export / map / scan
  • --json: machine-readable envelope on stdout
{
  "schemaVersion": "8",
  "defaults": {
    "env": {
      "EDITOR": { "value": "nvim" }
    },
    "shell": {
      "aliases": {
        "ll": { "value": "ls -lah" }
      }
    }
  },
  "targets": {
    "macos": {
      "packages": [
        { "id": "git", "prefer": "brew" },
        { "id": "ripgrep", "prefer": "brew" }
      ]
    },
    "ubuntu": {
      "packages": [
        { "id": "git", "prefer": "apt" }
      ],
      "env": {
        "EDITOR": null
      }
    },
    "windows": {
      "packages": [
        {
          "id": "git",
          "prefer": "winget",
          "managers": { "winget": "Git.Git", "scoop": "git", "choco": "git" }
        }
      ]
    }
  },
  "updates": {
    "enabled": true,
    "interval": "24h",
    "autoApply": false,
    "notify": true
  },
  "repo": {
    "url": "https://github.com/example/dotfiles",
    "ref": "main"
  }
}
  • Desired state lives under defaults and/or targets.<id>, not as top-level packages / env.
  • No per-record host in v8/v9 (use target buckets).
  • Target map entries may be null tombstones to drop a default for one OS.
  • repo, updates, and v8 adapters stay top-level. Prefer v8 unless a managed external recipe requires v9.
  • Schema v8 adapters declare plugin CLIs. Details are in SCHEMA.md.
  • Schema v9 managed external releases: docs/external-releases.md and SCHEMA.md.
  • Legacy v1-v7 specs still load. Convert with genv migrate.

Schema v10 adds local modules. genv.json registers module files by name under modules, and each target selects them with useModules:

{
  "schemaVersion": "10",
  "modules": {
    "base": "modules/base.json",
    "dev": "modules/dev.json"
  },
  "targets": {
    "macos": {
      "useModules": ["dev"],
      "packages": [{ "id": "ghostty" }]
    }
  }
}
// modules/base.json
{
  "schemaVersion": "10",
  "defaults": {
    "packages": [{ "id": "jq" }],
    "env": { "EDITOR": { "value": "nvim" } }
  }
}

// modules/dev.json
{
  "schemaVersion": "10",
  "requiresModules": ["base"],
  "defaults": { "packages": [{ "id": "ripgrep" }] }
}
  • Modules are local, trusted files. Paths resolve inside the spec's directory; a module that escapes it is refused.
  • useModules is additive: selecting dev also selects base through requiresModules. Here macOS gets ghostty, ripgrep, jq, and EDITOR.
  • There is no override syntax. An identical declaration in the root and a module is kept once; a conflicting one is an error naming both origins.
  • Module-owned resources are read-only from the CLI. Inspect a composition with genv config --target macos. genv migrate refuses v10. Details in SCHEMA.md.

v8 merges defaults with the active target (including tombstones), then reconciles packages, env, shell, files, services, and hooks.

Read

Reads the spec and the lock, then resolves --target / $GENV_TARGET / host class.

Foreign lock

Refuses a lock from the wrong target or OS. Recover with genv apply --force-new-lock.

Install / uninstall

Installs spec-only packages; uninstalls lock entries removed from the spec.

Lock

Updates the lock after each run. v8 records target + goos.

The v3 line closed the old milestone backlog. v4 is schema v8 multi-target configs plus schema v9 managed external releases, and schema v10 adds local modules and dependency-aware services. Future work lives in GitHub issues and ROADMAP.md.

Ready to get started?

GENV v4.7.4 is here. It is open-source and MIT licensed. Adapter quality and test coverage are where contributions matter most.