askill
mise-configuration

mise-configurationSafety 90Repository

Configure environment via mise [env] SSoT. TRIGGERS - mise env, mise.toml, environment variables, centralize config, Python venv, mise templates, hub-spoke architecture, monorepo structure, subfolder mise.toml.

17 stars
1.2k downloads
Updated 3/6/2026

Package Files

Loading files...
SKILL.md

mise Configuration as Single Source of Truth

Use mise [env] as centralized configuration with backward-compatible defaults.

When to Use This Skill

Use this skill when:

  • Centralizing environment variables in mise.toml
  • Setting up Python venv auto-creation with mise
  • Implementing hub-spoke configuration for monorepos
  • Creating backward-compatible environment patterns

Core Principle

Define all configurable values in .mise.toml [env] section. Scripts read via environment variables with fallback defaults. Same code path works WITH or WITHOUT mise installed.

Key insight: mise auto-loads [env] values when shell has mise activate configured. Scripts using os.environ.get("VAR", "default") pattern work identically whether mise is present or not.

Quick Reference

Language Patterns

LanguagePatternNotes
Pythonos.environ.get("VAR", "default")Returns string, cast if int
Bash${VAR:-default}Standard POSIX expansion
JavaScriptprocess.env.VAR || "default"Falsy check, watch for "0"
Goos.Getenv("VAR") with defaultEmpty string if unset
Ruststd::env::var("VAR").unwrap_or()Returns Result

Special Directives

DirectivePurposeExample
_.fileLoad from .env files_.file = ".env"
_.pathExtend PATH_.path = ["bin", "node_modules/.bin"]
_.sourceExecute bash scripts_.source = "./scripts/env.sh"
_.python.venvAuto-create Python venv_.python.venv = { path = ".venv", create = true }

Python Venv Auto-Creation (Critical)

Auto-create and activate Python virtual environments:

[env]
_.python.venv = { path = ".venv", create = true }

This pattern is used in ALL projects. When entering the directory with mise activated:

  1. Creates .venv if it doesn't exist
  2. Activates the venv automatically
  3. Works with uv for fast venv creation

Alternative via [settings]:

[settings]
python.uv_venv_auto = true

Hub-Spoke Architecture (CRITICAL)

Keep root mise.toml lean by delegating domain-specific tasks to subfolder mise.toml files.

Wiki Reference: Pattern-mise-Configuration - Complete documentation with CLAUDE.md footer prompt

When to Use

  • Root mise.toml exceeds ~50 lines
  • Project has multiple domains (packages, experiments, infrastructure)
  • Different subfolders need different task sets

Spoke Scenarios

Hub-spoke applies to any multi-domain project, not just packages:

ScenarioSpoke FoldersSpoke Tasks
Monorepopackages/api/, packages/web/build, test, lint, deploy
ML/Researchexperiments/exp-001/, training/, evaluation/train, evaluate, notebook, sweep
Infrastructureterraform/, kubernetes/, ansible/plan, apply, deploy, validate
Data Pipelineingestion/, transform/, export/extract, load, validate, export

Directory Structure Examples

Monorepo:

project/
├── mise.toml              # Hub: [tools] + [env] + orchestration
├── packages/
│   ├── api/mise.toml      # Spoke: API tasks
│   └── web/mise.toml      # Spoke: Web tasks
└── scripts/mise.toml      # Spoke: Utility scripts

ML/Research Project:

ml-project/
├── mise.toml              # Hub: python, cuda, orchestration
├── experiments/
│   ├── baseline/mise.toml # Spoke: baseline experiment
│   └── ablation/mise.toml # Spoke: ablation study
├── training/mise.toml     # Spoke: training pipelines
└── evaluation/mise.toml   # Spoke: metrics, benchmarks

Infrastructure:

infra/
├── mise.toml              # Hub: terraform, kubectl, helm
├── terraform/
│   ├── prod/mise.toml     # Spoke: production infra
│   └── staging/mise.toml  # Spoke: staging infra
└── kubernetes/mise.toml   # Spoke: k8s manifests

Hub Responsibilities (Root mise.toml)

# mise.toml - Hub: Keep this LEAN

[tools]
python = "<version>"
uv = "latest"

[env]
PROJECT_NAME = "my-project"
_.python.venv = { path = ".venv", create = true }

# Orchestration: delegate to spokes
[tasks.train-all]
run = """
cd experiments/baseline && mise run train
cd experiments/ablation && mise run train
"""

[tasks."build:api"]
run = "cd packages/api && mise run build"

Spoke Responsibilities (Subfolder mise.toml)

# experiments/baseline/mise.toml - Spoke

[env]
EXPERIMENT_NAME = "baseline"
EPOCHS = "<num>"           # e.g., 100
LEARNING_RATE = "<float>"  # e.g., 0.001

[tasks.train]
run = "uv run python train.py"
sources = ["*.py", "config.yaml"]
outputs = ["checkpoints/*.pt"]

[tasks.evaluate]
depends = ["train"]
run = "uv run python evaluate.py"

Inheritance Rules

  • Spoke mise.toml inherits hub's [tools] automatically
  • Spoke [env] extends hub's [env] (can override per domain)
  • .mise.local.toml applies at directory level (secrets stay local)

Anti-Patterns

Anti-PatternProblemFix
All tasks in rootRoot grows to 200+ linesDelegate to spoke files
Duplicated [tools]Version drift between spokesDefine [tools] only in hub
Spoke defines runtimesConflicts with hubSpokes inherit hub's [tools]
No orchestrationMust cd manuallyHub orchestrates spoke tasks

Monorepo Workspace Pattern

For Python monorepos using uv workspaces, the venv is created at the workspace root. Sub-packages share the root venv.

# Root mise.toml
[env]
_.python.venv = { path = ".venv", create = true }

Hoisted Dev Dependencies (PEP 735)

Dev dependencies (pytest, ruff, jupyterlab, etc.) should be hoisted to workspace root pyproject.toml using [dependency-groups]:

# SSoT-OK: example workspace configuration
# Root pyproject.toml
[tool.uv.workspace]
members = ["packages/*"]

[dependency-groups]
dev = [
    "pytest>=<version>",
    "ruff>=<version>",
    "jupyterlab>=<version>",
]

Why hoist? Sub-package [dependency-groups] are NOT automatically installed by uv sync from root. Hoisting ensures:

  • Single command: uv sync --group dev
  • No "unnecessary package" warnings
  • Unified dev environment across all packages

Reference: bootstrap-monorepo.md for complete workspace setup

Special Directives

Load from .env Files (_.file)

[env]
# Single file
_.file = ".env"

# Multiple files with options
_.file = [
    ".env",
    { path = ".env.secrets", redact = true }
]

Extend PATH (_.path)

[env]
_.path = [
    "{{config_root}}/bin",
    "{{config_root}}/node_modules/.bin",
    "scripts"
]

Source Bash Scripts (_.source)

[env]
_.source = "./scripts/env.sh"
_.source = { path = ".secrets.sh", redact = true }

Lazy Evaluation (tools = true)

By default, env vars resolve BEFORE tools install. Use tools = true to access tool-generated paths:

[env]
# Access PATH after tools are set up
GEM_BIN = { value = "{{env.GEM_HOME}}/bin", tools = true }

# Load .env files after tool setup
_.file = { path = ".env", tools = true }

Template Syntax (Tera)

mise uses Tera templating. Delimiters: {{ }} expressions, {% %} statements, {# #} comments.

Built-in Variables

VariableDescription
{{config_root}}Directory containing .mise.toml
{{cwd}}Current working directory
{{env.VAR}}Environment variable
{{mise_bin}}Path to mise binary
{{mise_pid}}mise process ID
{{xdg_cache_home}}XDG cache directory
{{xdg_config_home}}XDG config directory
{{xdg_data_home}}XDG data directory

Functions

[env]
# Get env var with fallback
NODE_VER = "{{ get_env(name='NODE_VERSION', default='20') }}"

# Execute shell command
TIMESTAMP = "{{ exec(command='date +%Y-%m-%d') }}"

# System info
ARCH = "{{ arch() }}"      # x64, arm64
OS = "{{ os() }}"          # linux, macos, windows
CPUS = "{{ num_cpus() }}"

# File operations
VERSION = "{{ read_file(path='VERSION') | trim }}"
HASH = "{{ hash_file(path='config.json', len=8) }}"

Filters

[env]
# Case conversion
SNAKE = "{{ name | snakecase }}"
KEBAB = "{{ name | kebabcase }}"
CAMEL = "{{ name | lowercamelcase }}"

# String manipulation
TRIMMED = "{{ text | trim }}"
UPPER = "{{ text | upper }}"
REPLACED = "{{ text | replace(from='old', to='new') }}"

# Path operations
ABSOLUTE = "{{ path | absolute }}"
BASENAME = "{{ path | basename }}"
DIRNAME = "{{ path | dirname }}"

Conditionals

[env]
{% if env.DEBUG %}
LOG_LEVEL = "debug"
{% else %}
LOG_LEVEL = "info"
{% endif %}

Required & Redacted Variables

Required Variables

Enforce variable definition with helpful messages:

[env]
DATABASE_URL = { required = true }
API_KEY = { required = "Get from https://example.com/api-keys" }

Redacted Variables

Hide sensitive values from output:

[env]
SECRET = { value = "my_secret", redact = true }
_.file = { path = ".env.secrets", redact = true }

# Pattern-based redactions
redactions = ["*_TOKEN", "*_KEY", "PASSWORD"]

[settings] Section

[settings]
experimental = true              # Enable experimental features
python.uv_venv_auto = true       # Auto-create venv with uv

[tools] Version Pinning

Pin tool versions for reproducibility:

[tools]
python = "3.11"  # minimum baseline; use 3.12, 3.13 as needed
node = "latest"
uv = "latest"

# With options
rust = { version = "1.75", profile = "minimal" }

min_version: Enforce mise version compatibility:

min_version = "2024.9.5"

Implementation Steps

  1. Identify hardcoded values - timeouts, paths, thresholds, feature flags
  2. Create .mise.toml - add [env] section with documented variables
  3. Add venv auto-creation - _.python.venv = { path = ".venv", create = true }
  4. Update scripts - use env vars with original values as defaults
  5. Add ADR reference - comment: # ADR: 2025-12-08-mise-env-centralized-config
  6. Test without mise - verify script works using defaults
  7. Test with mise - verify activated shell uses .mise.toml values

GitHub Token Multi-Account Patterns (MANDATORY for Multi-Account Setups) {#github-token-multi-account-patterns}

For multi-account GitHub setups, mise [env] provides per-directory token configuration that overrides gh CLI's global authentication.

Token Storage

Store tokens in a centralized, secure location:

mkdir -p ~/.claude/.secrets
chmod 700 ~/.claude/.secrets

# Create token files (one per account)
gh auth login  # authenticate as account
gh auth token > ~/.claude/.secrets/gh-token-accountname
chmod 600 ~/.claude/.secrets/gh-token-*

Per-Directory Configuration

# ~/.claude/.mise.toml (terrylica account)
[env]
GH_TOKEN = "{{ read_file(path=config_root ~ '/.secrets/gh-token-terrylica') | trim }}"
GITHUB_TOKEN = "{{ read_file(path=config_root ~ '/.secrets/gh-token-terrylica') | trim }}"
GH_ACCOUNT = "terrylica"  # For human reference only
# ~/eon/.mise.toml (terrylica account - different directory)
[env]
GH_TOKEN = "{{ read_file(path=env.HOME ~ '/.claude/.secrets/gh-token-terrylica') | trim }}"
GITHUB_TOKEN = "{{ read_file(path=env.HOME ~ '/.claude/.secrets/gh-token-terrylica') | trim }}"
GH_ACCOUNT = "terrylica"

Variable Naming Convention

VariableUsage ContextExample
GH_TOKENmise [env], Doppler, verification tasks.mise.toml, shell scripts
GITHUB_TOKENnpm scripts, GitHub Actions, semantic-releasepackage.json, workflows

Rule: Always set BOTH variables in mise [env] pointing to the same token file. Different tools check different variable names.

Alternative: 1Password Integration

For enhanced security with automatic token rotation:

[env]
GH_TOKEN = "{{ op_read('op://Engineering/GitHub Token/credential') }}"

With caching for performance:

[env]
GH_TOKEN = "{{ cache(key='gh_token', duration='1h', run='op read op://Engineering/GitHub Token/credential') }}"

Verification

/usr/bin/env bash << 'MISE_EOF'
for dir in ~/.claude ~/eon ~/own ~/scripts ~/459ecs; do
  cd "$dir" && eval "$(mise hook-env -s bash)" && echo "$dir → $GH_ACCOUNT"
done
MISE_EOF

Pattern: Based on GitHub Multi-Account Authentication ADR (mise [env] per-directory token loading).

SSH ControlMaster Warning: If using multi-account SSH, ensure ControlMaster no is set for GitHub hosts in ~/.ssh/config. Cached connections can authenticate with the wrong account. See SSH ControlMaster Cache for troubleshooting.

Anti-Patterns

Anti-PatternWhyInstead
mise exec -- script.pyForces mise dependencyUse env vars with defaults
Secrets in .mise.tomlVisible in repoUse Doppler or redact = true
No defaults in scriptsBreaks without miseAlways provide fallback
Mixing env/tools resolutionOrder mattersUse tools = true for tool-dependent vars
[env] secrets for pueue jobsPueue runs clean shell, no miseUse python-dotenv + .env file
__MISE_DIFF leaks via SSHRemote trust errorsunset __MISE_DIFF before SSH

Critical: mise [env] Secrets and Non-Interactive Shells

Do NOT put secrets in mise.toml [env] if they will be consumed by pueue jobs, cron jobs, or systemd services. These execution contexts run in clean shells without mise activation — [env] variables are invisible.

Preferred pattern: Use mise.toml for task definitions and non-secret configuration only. Put secrets in a .env file (gitignored) and load them with python-dotenv at runtime:

# mise.toml — tasks only, no secrets in [env]
[env]
DATABASE_NAME = "mydb"       # OK: non-secret defaults

[tasks.backfill]
run = "bash scripts/backfill.sh"
# .env (gitignored) — secrets loaded by python-dotenv
API_KEY=sk-abc123
DATABASE_PASSWORD=hunter2

This works identically in interactive shells, pueue jobs, cron, systemd, and across macOS/Linux.

Cross-reference: See devops-tools:distributed-job-safety — G-15, AP-16

Task Orchestration Integration

When you detect workflow opportunities during mise configuration, prescriptively invoke the mise-tasks skill.

Detection Triggers

  • Multi-step workflows (test + build + deploy)
  • Repeatable project commands (lint, format, release)
  • Dependency chains (db-migrate depends on db-init)
  • File-tracked builds (sources/outputs caching)

Prescriptive AI Reminder

PRESCRIPTIVE: When detecting multi-step project workflows, invoke mise-tasks skill to generate appropriate [tasks] definitions with dependency management.

Example: From Environment to Tasks

Step 1: Configure environment (this skill):

[env]
DATABASE_URL = "postgresql://localhost/mydb"
_.python.venv = { path = ".venv", create = true }

Step 2: Define tasks (mise-tasks skill):

[tasks.test]
depends = ["lint"]
run = "pytest tests/"

[tasks.deploy]
depends = ["test", "build"]
run = "deploy.sh"

Tasks automatically inherit [env] values.


Additional Resources

For complete code patterns and examples, see: references/patterns.md

For task orchestration, see: mise-tasks skill - Dependencies, arguments, file tracking, watch mode

Wiki Documentation: Pattern-mise-Configuration - Copyable CLAUDE.md footer prompt, hub-spoke architecture, quick reference

ADR Reference: When implementing mise configuration, create an ADR at docs/adr/YYYY-MM-DD-mise-env-centralized-config.md in your project.


Troubleshooting

IssueCauseSolution
Env vars not loadingmise not activatedAdd mise activate to shell rc file
Venv not createdPython not installedRun mise install python
Tasks not foundWrong mise.toml locationEnsure mise.toml is in project root
PATH not updatedShims not in PATHAdd mise shims to ~/.zshenv
_.file not loading.env file missingCreate .env file or remove _.file directive
Subfolder config ignoredMissing min_versionAdd min_version to subfolder mise.toml

Install

Download ZIP
Requires askill CLI v1.0+▶

AI Quality Score

90/100Analyzed 2/22/2026

Metadata

Licenseunknown
Version-
Updated3/6/2026
Publisherterrylica

Tags

apici-cddatabasegithubgithub-actionslintingllmobservabilitypromptingsecuritytesting