home / skills / terrylica / cc-skills / mise-configuration

mise-configuration skill

/plugins/itp/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-configuration

Review the files below or copy the command above to add this skill to your agents.

Files (3)
SKILL.md
18.6 KB
---
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       |

Overview

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.

How this skill works

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.

When to use it

  • You need a centralized environment variables SSoT across a repo or monorepo.
  • You want automatic Python venv creation and activation at workspace root or per-project.
  • You have a multi-domain project and want hub-spoke task delegation to keep root mise.toml lean.
  • You must support multi-account GitHub tokens per directory and ensure tools pick the right env var.
  • You need templated, lazy, or tool-aware env values (files, commands, computed values).

Best practices

  • Define all configurable values in .mise.toml [env] and keep defaults in code using env.get patterns.
  • Pin [tools] at the hub and let spokes inherit to avoid version drift.
  • Hoist dev dependencies to the workspace root pyproject.toml for consistent dev environments.
  • Delegate domain-specific tasks to spoke mise.toml files when root exceeds ~50 lines.
  • Use _.file, _.source, and _.path directives for structured env loading and PATH extension.

Example use cases

  • Monorepo: hub mise.toml defines python venv and orchestration; packages/* spokes define build/test tasks.
  • ML project: experiments spokes declare EPOCHS and LEARNING_RATE while hub manages python/tool versions.
  • Infra repo: root manages terraform and kubectl versions; env tokens and secrets are loaded per-spoke.
  • Multi-account GitHub: per-directory GH_TOKEN and GITHUB_TOKEN loaded from secure token files or vault.
  • Local-first workflows: scripts use env fallbacks so CI or developers without mise still run reliably.

FAQ

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.