home / skills / terrylica / cc-skills / mise-configuration
This skill centralizes environment configuration using mise env as the single source of truth, simplifying cross-project consistency and automation benefits.
npx playbooks add skill terrylica/cc-skills --skill mise-configurationReview the files below or copy the command above to add this skill to your agents.
---
name: mise-configuration
description: 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.
allowed-tools: Read, Bash, Glob, Grep, Edit, Write
---
# 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
| Language | Pattern | Notes |
| ---------- | ---------------------------------- | --------------------------- |
| Python | `os.environ.get("VAR", "default")` | Returns string, cast if int |
| Bash | `${VAR:-default}` | Standard POSIX expansion |
| JavaScript | `process.env.VAR \|\| "default"` | Falsy check, watch for "0" |
| Go | `os.Getenv("VAR")` with default | Empty string if unset |
| Rust | `std::env::var("VAR").unwrap_or()` | Returns Result<String> |
### Special Directives
| Directive | Purpose | Example |
| --------------- | ----------------------- | --------------------------------------------------- |
| `_.file` | Load from .env files | `_.file = ".env"` |
| `_.path` | Extend PATH | `_.path = ["bin", "node_modules/.bin"]` |
| `_.source` | Execute bash scripts | `_.source = "./scripts/env.sh"` |
| `_.python.venv` | Auto-create Python venv | `_.python.venv = { path = ".venv", create = true }` |
## Python Venv Auto-Creation (Critical)
Auto-create and activate Python virtual environments:
```toml
[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]**:
```toml
[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](https://github.com/terrylica/cc-skills/wiki/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:
| Scenario | Spoke Folders | Spoke Tasks |
| ------------------ | -------------------------------------------------- | -------------------------------- |
| **Monorepo** | `packages/api/`, `packages/web/` | build, test, lint, deploy |
| **ML/Research** | `experiments/exp-001/`, `training/`, `evaluation/` | train, evaluate, notebook, sweep |
| **Infrastructure** | `terraform/`, `kubernetes/`, `ansible/` | plan, apply, deploy, validate |
| **Data Pipeline** | `ingestion/`, `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`)
```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`)
```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-Pattern | Problem | Fix |
| ---------------------- | ---------------------------- | ---------------------------- |
| All tasks in root | Root grows to 200+ lines | Delegate to spoke files |
| Duplicated [tools] | Version drift between spokes | Define [tools] only in hub |
| Spoke defines runtimes | Conflicts with hub | Spokes inherit hub's [tools] |
| No orchestration | Must cd manually | Hub 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.
```toml
# 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]`:
```toml
# 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](../mise-tasks/references/bootstrap-monorepo.md#root-pyprojecttoml-workspace) for complete workspace setup
## Special Directives
### Load from .env Files (`_.file`)
```toml
[env]
# Single file
_.file = ".env"
# Multiple files with options
_.file = [
".env",
{ path = ".env.secrets", redact = true }
]
```
### Extend PATH (`_.path`)
```toml
[env]
_.path = [
"{{config_root}}/bin",
"{{config_root}}/node_modules/.bin",
"scripts"
]
```
### Source Bash Scripts (`_.source`)
```toml
[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:
```toml
[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
| Variable | Description |
| --------------------- | ------------------------------- |
| `{{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
```toml
[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
```toml
[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
```toml
[env]
{% if env.DEBUG %}
LOG_LEVEL = "debug"
{% else %}
LOG_LEVEL = "info"
{% endif %}
```
## Required & Redacted Variables
### Required Variables
Enforce variable definition with helpful messages:
```toml
[env]
DATABASE_URL = { required = true }
API_KEY = { required = "Get from https://example.com/api-keys" }
```
### Redacted Variables
Hide sensitive values from output:
```toml
[env]
SECRET = { value = "my_secret", redact = true }
_.file = { path = ".env.secrets", redact = true }
# Pattern-based redactions
redactions = ["*_TOKEN", "*_KEY", "PASSWORD"]
```
## [settings] Section
```toml
[settings]
experimental = true # Enable experimental features
python.uv_venv_auto = true # Auto-create venv with uv
```
## [tools] Version Pinning
Pin tool versions for reproducibility:
```toml
[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:
```toml
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:
```bash
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
```toml
# ~/.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
```
```toml
# ~/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
| Variable | Usage Context | Example |
| -------------- | --------------------------------------------- | --------------------------- |
| `GH_TOKEN` | mise [env], Doppler, verification tasks | `.mise.toml`, shell scripts |
| `GITHUB_TOKEN` | npm scripts, GitHub Actions, semantic-release | `package.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:
```toml
[env]
GH_TOKEN = "{{ op_read('op://Engineering/GitHub Token/credential') }}"
```
With caching for performance:
```toml
[env]
GH_TOKEN = "{{ cache(key='gh_token', duration='1h', run='op read op://Engineering/GitHub Token/credential') }}"
```
### Verification
```bash
/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](../semantic-release/references/troubleshooting.md#ssh-controlmaster-cache) for troubleshooting.
## Anti-Patterns
| Anti-Pattern | Why | Instead |
| ------------------------------ | ------------------------------- | ------------------------------------------ |
| `mise exec -- script.py` | Forces mise dependency | Use env vars with defaults |
| Secrets in `.mise.toml` | Visible in repo | Use Doppler or `redact = true` |
| No defaults in scripts | Breaks without mise | Always provide fallback |
| Mixing env/tools resolution | Order matters | Use `tools = true` for tool-dependent vars |
| `[env]` secrets for pueue jobs | Pueue runs clean shell, no mise | Use `python-dotenv` + `.env` file |
| `__MISE_DIFF` leaks via SSH | Remote trust errors | `unset __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:
```toml
# mise.toml — tasks only, no secrets in [env]
[env]
DATABASE_NAME = "mydb" # OK: non-secret defaults
[tasks.backfill]
run = "bash scripts/backfill.sh"
```
```bash
# .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](../../devops-tools/skills/distributed-job-safety/references/environment-gotchas.md), [AP-16](../../devops-tools/skills/distributed-job-safety/SKILL.md)
## 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](../mise-tasks/SKILL.md)** to generate appropriate `[tasks]` definitions with dependency management.
### Example: From Environment to Tasks
**Step 1**: Configure environment (this skill):
```toml
[env]
DATABASE_URL = "postgresql://localhost/mydb"
_.python.venv = { path = ".venv", create = true }
```
**Step 2**: Define tasks (`mise-tasks` skill):
```toml
[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`](./references/patterns.md)**
**For task orchestration**, see: **[`mise-tasks` skill](../mise-tasks/SKILL.md)** - Dependencies, arguments, file tracking, watch mode
**Wiki Documentation**: [Pattern-mise-Configuration](https://github.com/terrylica/cc-skills/wiki/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
| Issue | Cause | Solution |
| ------------------------ | ------------------------ | -------------------------------------------- |
| Env vars not loading | mise not activated | Add mise activate to shell rc file |
| Venv not created | Python not installed | Run `mise install python` |
| Tasks not found | Wrong mise.toml location | Ensure mise.toml is in project root |
| PATH not updated | Shims not in PATH | Add mise shims to ~/.zshenv |
| \_.file not loading | .env file missing | Create .env file or remove \_.file directive |
| Subfolder config ignored | Missing min_version | Add min_version to subfolder mise.toml |
This skill configures a project environment using mise [env] as the single source of truth. It centralizes environment variables, auto-creates Python virtual environments, and supports hub‑spoke layouts for monorepos so each domain can inherit or override settings. The patterns are backward compatible: scripts use normal env fallbacks and work whether mise is installed or not.
Place all configurable values under the .mise.toml [env] section and use standard env access patterns in code (e.g., os.environ.get(...), process.env, ${VAR:-default}). mise will auto-load those values when the shell runs mise activate, create venvs per workspace rules, and apply templating, file loading, and redaction directives. Hub (root) mise.toml supplies shared [tools] and [env]; subfolder spokes extend or override env and declare domain tasks.
Will scripts still run if mise is not installed?
Yes. Code should use env-access patterns with sensible defaults so the same path works with or without mise.
Where should I pin tool versions?
Pin [tools] in the hub (root) mise.toml so spokes inherit a single source of runtime versions.