The template system generates machine-specific configuration files from templates. This enables different configs for work vs personal machines, different OS configurations, and consistent settings across multiple machines.
Version: 1.0.0
# 1. Initialize template variables
blackdot template init
# 2. Edit your configuration
blackdot template edit
# 3. Review detected values
blackdot template vars
# 4. Generate configuration files
blackdot template render
# 5. Create symlinks to destinations
blackdot template linkTemplate file: templates/configs/gitconfig.tmpl
[user]
name = {{ GIT_USER_NAME }}
email = {{ GIT_EMAIL }}
{{#if GIT_SIGNING_KEY}}
signingkey = {{ GIT_SIGNING_KEY }}
{{/if}}
[core]
editor = {{ EDITOR | default="vim" }}
{{#if IS_WORK_MACHINE}}
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
{{/if}}Variables file: templates/_variables_work-laptop.sh
export GIT_USER_NAME="John Doe"
export GIT_EMAIL="john.doe@company.com"
export GIT_SIGNING_KEY="ABC123DEF456"
export IS_WORK_MACHINE="true"
export EDITOR="code"Result: generated/gitconfig (different per machine)
Template: templates/configs/ssh-config.tmpl
{{#each SSH_HOSTS}}
Host {{ this.name }}
HostName {{ this.hostname }}
User {{ this.user }}
IdentityFile {{ this.key }}
{{/each}}
Variables: templates/_variables_personal.sh
export SSH_HOSTS='[
{"name": "github", "hostname": "github.com", "user": "git", "key": "~/.ssh/id_ed25519_personal"},
{"name": "homelab", "hostname": "192.168.1.100", "user": "admin", "key": "~/.ssh/id_rsa_homelab"}
]'Template: templates/configs/env.secrets.tmpl
# API Keys (different per environment)
export OPENAI_API_KEY="{{ OPENAI_API_KEY }}"
export ANTHROPIC_API_KEY="{{ ANTHROPIC_API_KEY }}"
{{#if IS_WORK_MACHINE}}
# Work-specific secrets
export COMPANY_VPN_TOKEN="{{ COMPANY_VPN_TOKEN }}"
export AWS_PROFILE="work"
{{else}}
# Personal secrets
export AWS_PROFILE="personal"
{{/if}}- Templates in
templates/configs/*.tmplcontain placeholders like{{ variable }} - Variables are sourced from multiple locations with defined precedence
- Rendering substitutes variables and processes conditionals
- Generated files are saved to
generated/directory - Symlinks connect generated files to their final destinations
blackdot/
├── templates/
│ ├── _variables.sh # Default variable definitions
│ ├── _variables.local.sh # Your machine-specific overrides (gitignored)
│ ├── _variables.local.sh.example # Example to copy from
│ ├── _arrays.local.json # JSON arrays for {{#each}} loops (gitignored)
│ ├── _arrays.local.json.example # Example JSON arrays
│ └── configs/ # Template files
│ ├── gitconfig.tmpl # → ~/.gitconfig
│ ├── 99-local.zsh.tmpl # → zsh/zsh.d/99-local.zsh
│ ├── ssh-config.tmpl # → ~/.ssh/config
│ └── claude.local.tmpl # → ~/.claude.local
│
└── generated/ # Rendered output (gitignored)
├── gitconfig
├── 99-local.zsh
├── ssh-config
└── claude.local
Interactive setup wizard that:
- Detects your system (hostname, OS, architecture)
- Creates
_variables.local.shfrom the example - Opens the file in your editor
blackdot template initDisplay all template variables and their current values:
# Show all variables with values
blackdot template vars
# Show variable names only
blackdot template vars --quietOutput example:
Template Variables
──────────────────────────────────────
Auto-detected:
hostname = macbook-pro
os = macos
arch = arm64
user = john
machine_type = personal
User-configured:
git_email = john@example.com
git_name = John Doe
aws_profile = default
Render templates to the generated/ directory:
# Render all templates
blackdot template render
# Preview without writing files
blackdot template render --dry-run
# Force re-render even if up to date
blackdot template render --force
# Render specific template
blackdot template render gitconfigCreate symlinks from generated files to their destinations:
# Create symlinks
blackdot template link
# Preview without creating
blackdot template link --dry-runDestination mapping:
| Generated File | Destination |
|---|---|
gitconfig |
~/.gitconfig |
99-local.zsh |
zsh/zsh.d/99-local.zsh |
ssh-config |
~/.ssh/config |
claude.local |
~/.claude.local |
Validate template syntax without rendering:
blackdot template checkChecks for:
- Unmatched
{{#if}}/{{/if}}blocks - Unclosed variable tags
- Malformed syntax
Show differences between templates and generated files:
# Quick check
blackdot template diff
# Show detailed diffs
blackdot template diff --verboseOpen _variables.local.sh in your editor:
blackdot template editList available templates and their status:
blackdot template listOutput:
Available Templates
───────────────────────────────────────
gitconfig ✓ up to date
99-local.zsh ○ stale
ssh-config ✗ not generated
Manage JSON/shell arrays for {{#each}} loops:
# View loaded arrays and their source
blackdot template arrays
# Validate JSON arrays file syntax
blackdot template arrays --validate
# Export shell arrays to JSON format
blackdot template arrays --export-jsonOutput example:
Template Arrays
──────────────────────────────────────
Source: json (templates/_arrays.local.json)
ssh_hosts (3 items):
• github | github.com | git | ~/.ssh/id_ed25519
• gitlab | gitlab.com | git | ~/.ssh/id_ed25519
• work-server | server.company.com | deploy | ~/.ssh/id_work | ProxyJump bastion
Sync template variables with your vault for cross-machine portability:
# Push local variables to vault
blackdot template vault push
# Pull from vault to local
blackdot template vault pull
# Show differences between local and vault
blackdot template vault diff
# Bidirectional sync with conflict detection
blackdot template vault sync
# Check sync status
blackdot template vault statusOptions:
| Option | Description |
|---|---|
--force, -f |
Force overwrite without confirmation |
--prefer-local |
On conflict, use local file |
--prefer-vault |
On conflict, use vault item |
--no-backup |
Don't backup local file on pull |
Example workflow:
# First machine: backup your config to vault
blackdot template init
blackdot template vault push
# New machine: restore from vault
blackdot vault pull # Get vault access
blackdot template vault pull # Pull template variables
blackdot template render # Generate configsUse {{ variable }} to insert a variable value:
[user]
name = {{ git_name }}
email = {{ git_email }}
Both {{ var }} (with spaces) and {{var}} (without spaces) work.
Transform variable values using filters with the pipe (|) syntax:
{{ variable | filter }}
{{ variable | filter "argument" }}
{{ variable | filter1 | filter2 }}
| Filter | Description | Example |
|---|---|---|
upper |
Convert to UPPERCASE | {{ hostname | upper }} → MACBOOK-PRO |
lower |
Convert to lowercase | {{ hostname | lower }} → macbook-pro |
capitalize |
Capitalize first letter | {{ name | capitalize }} → John |
trim |
Remove leading/trailing whitespace | {{ value | trim }} |
default "value" |
Use fallback if variable is empty | {{ editor | default "vim" }} |
replace "old,new" |
Replace all occurrences | {{ email | replace "@,_at_" }} → user_at_example.com |
append "text" |
Append text to value | {{ name | append ".local" }} |
prepend "text" |
Prepend text to value | {{ name | prepend "user-" }} |
quote |
Wrap in double quotes | {{ path | quote }} → "/path/to/file" |
squote |
Wrap in single quotes | {{ path | squote }} → '/path/to/file' |
truncate N |
Truncate to N characters | {{ hostname | truncate 5 }} → macbo |
length |
Return string length | {{ name | length }} → 4 |
basename |
Extract filename from path | {{ home | basename }} → john |
dirname |
Extract directory from path | {{ home | dirname }} → /Users |
Apply multiple filters in sequence:
# Uppercase hostname truncated to 8 characters
Host: {{ hostname | upper | truncate 8 }}
# Default value, then uppercase
Editor: {{ VISUAL | default "vim" | upper }}
Default values for optional settings:
export EDITOR="{{ editor | default "vim" }}"
export AWS_PROFILE="{{ aws_profile | default "default" }}"
Path manipulation:
# Extract just the username from home path
User: {{ home | basename }}
# → john
# Get parent directory
Parent: {{ workspace | dirname }}
# → /Users/john
Consistent formatting:
# Hostname as uppercase identifier
export MACHINE_ID="{{ hostname | upper | replace "-,_" }}"
# → MACBOOK_PRO
# Email obfuscation in comments
# Contact: {{ git_email | replace "@, [at] " }}
# → john [at] example.com
String wrapping:
# For shell safety
alias home='cd {{ home | squote }}'
# Show all filters with descriptions
blackdot template filtersInclude content only if a variable has a non-empty value:
{{#if git_signing_key }}
[commit]
gpgsign = true
{{/if}}
Check if a variable equals a specific value:
{{#if machine_type == "work" }}
# Work-specific configuration
export WORK_EMAIL="{{ git_email }}"
{{/if}}
Provide alternative content:
{{#if bedrock_profile }}
export CLAUDE_USE_BEDROCK=1
{{#else}}
# Using default Anthropic API
{{/if}}
Include content only if a variable is empty:
{{#unless git_signing_key }}
# GPG signing not configured
{{/unless}}
Use {{#each}} to iterate over arrays with named fields:
{{#each ssh_hosts}}
Host {{ name }}
HostName {{ hostname }}
User {{ user }}
IdentityFile {{ identity }}
{{#if extra }} {{ extra }}
{{/if}}
{{/each}}
Available arrays:
| Array | Fields | Defined In |
|---|---|---|
ssh_hosts |
name, hostname, user, identity, extra |
_variables.local.sh |
Defining arrays:
# In _variables.local.sh
# Format: name|hostname|user|identity_file|extra_options
SSH_HOSTS=(
"github|github.com|git|~/.ssh/id_ed25519|"
"work-server|server.company.com|deploy|~/.ssh/id_work|ProxyJump bastion"
)Conditionals work inside loops - use {{#if extra }} to include optional fields only when set.
For cleaner array definitions, use JSON format instead of shell arrays:
Create templates/_arrays.local.json:
{
"ssh_hosts": [
{
"name": "github",
"hostname": "github.com",
"user": "git",
"identity": "~/.ssh/id_ed25519"
},
{
"name": "gitlab",
"hostname": "gitlab.com",
"user": "git",
"identity": "~/.ssh/id_ed25519"
},
{
"name": "work-server",
"hostname": "server.company.com",
"user": "deploy",
"identity": "~/.ssh/id_work",
"extra": "ProxyJump bastion"
}
]
}Benefits of JSON arrays:
- Easier to read and maintain
- No shell escaping issues
- Can be validated with standard JSON tools
- Optional fields can be omitted entirely
Managing arrays:
# View loaded arrays (shows source: JSON or shell)
blackdot template arrays
# Validate JSON syntax
blackdot template arrays --validate
# Export existing shell arrays to JSON format
blackdot template arrays --export-jsonThe template system automatically prefers JSON arrays if _arrays.local.json exists and jq is available. Falls back to shell arrays otherwise.
| Variable | Description | Example |
|---|---|---|
hostname |
Short hostname | macbook-pro |
hostname_full |
Full hostname | macbook-pro.local |
os |
Operating system | macos, linux, wsl |
os_family |
OS family | Darwin, Linux |
arch |
Architecture | arm64, amd64 |
user |
Username | john |
home |
Home directory | /Users/john |
workspace |
Workspace path | /Users/john/workspace |
machine_type |
Detected type | work, personal, unknown |
date |
Current date | 2024-01-15 |
datetime |
Current datetime | 2024-01-15 14:30:00 |
| Variable | Description | Default |
|---|---|---|
git_name |
Git author name | (empty) |
git_email |
Git author email | (empty) |
git_signing_key |
GPG key ID | (empty) |
git_default_branch |
Default branch | main |
git_editor |
Git editor | nvim |
aws_profile |
Default AWS profile | default |
aws_region |
Default AWS region | us-east-1 |
bedrock_profile |
AWS Bedrock profile | (empty) |
bedrock_region |
Bedrock region | us-west-2 |
editor |
Default editor | nvim |
visual |
Visual editor | code |
github_user |
GitHub username | (empty) |
github_enterprise_host |
GHE hostname | (empty) |
enable_nvm |
Enable NVM | true |
enable_pyenv |
Enable pyenv | false |
enable_k8s_prompt |
K8s in prompt | false |
Variables are resolved in this order (highest priority first):
-
Environment variables (
BLACKDOT_TMPL_*)BLACKDOT_TMPL_GIT_EMAIL="other@example.com" blackdot template render -
Local overrides (
templates/_variables.local.sh)TMPL_DEFAULTS[git_email]="john@example.com" -
Machine-type defaults (
TMPL_WORKorTMPL_PERSONAL)TMPL_WORK[git_email]="john@company.com" TMPL_PERSONAL[git_email]="john@personal.com"
-
Global defaults (
templates/_variables.sh)TMPL_DEFAULTS[git_editor]="nvim" -
Auto-detected values (hostname, OS, etc.)
The template system automatically detects your machine type based on:
- Environment variable:
BLACKDOT_MACHINE_TYPE - Hostname patterns:
- Contains
work,corp,office→work - Contains
personal,home,macbook,imac→personal
- Contains
- Directory indicators:
~/workor~/corpexists →work~/personalor~/.personal-machineexists →personal
Override detection in _variables.local.sh:
TMPL_AUTO[machine_type]="work"# templates/_variables.local.sh
TMPL_DEFAULTS[git_name]="John Doe"
TMPL_DEFAULTS[git_email]="john@example.com"# templates/_variables.local.sh
# Shared settings
TMPL_DEFAULTS[git_name]="John Doe"
TMPL_DEFAULTS[github_user]="johndoe"
# Work overrides (applied when machine_type == "work")
TMPL_WORK[git_email]="john.doe@company.com"
TMPL_WORK[aws_profile]="work-sso"
TMPL_WORK[github_enterprise_host]="github.company.com"
# Personal overrides (applied when machine_type == "personal")
TMPL_PERSONAL[git_email]="john@personal.com"
TMPL_PERSONAL[aws_profile]="personal"# templates/_variables.local.sh
# Force machine type (optional)
# TMPL_AUTO[machine_type]="work"
# Git
TMPL_DEFAULTS[git_name]="John Doe"
TMPL_DEFAULTS[git_email]="john@example.com"
TMPL_DEFAULTS[git_signing_key]="ABC123DEF456"
# AWS
TMPL_DEFAULTS[aws_profile]="default"
TMPL_DEFAULTS[aws_region]="us-west-2"
# AWS Bedrock (for Claude Code)
TMPL_DEFAULTS[bedrock_profile]="bedrock-profile"
TMPL_DEFAULTS[bedrock_region]="us-west-2"
# GitHub
TMPL_DEFAULTS[github_user]="johndoe"
# Work machine overrides
TMPL_WORK[git_email]="john.doe@company.com"
TMPL_WORK[aws_profile]="work-sso"
TMPL_WORK[github_enterprise_host]="github.company.com"
TMPL_WORK[github_enterprise_user]="jdoe"
# Feature toggles
TMPL_DEFAULTS[enable_nvm]="true"
TMPL_DEFAULTS[enable_pyenv]="true"
TMPL_DEFAULTS[enable_k8s_prompt]="true"Generates ~/.gitconfig with:
- User identity (name, email)
- GPG signing (if
git_signing_keyset) - Editor configuration
- Delta diff viewer settings
- Git aliases
- GitHub Enterprise URL rewriting (if configured)
- Work-specific includes (if
machine_type == "work")
Generates machine-specific shell configuration:
- Machine type exports
- Work/personal-specific aliases
- AWS profile configuration
- Bedrock settings (for Claude Code)
- Editor exports
- Custom path aliases
- Feature toggles (NVM, pyenv, rbenv, SDKMAN)
Generates SSH configuration:
- Global defaults (keepalive, keychain)
- GitHub host configuration
- GitHub Enterprise (if configured)
- Work-specific hosts (if work machine)
- Include for local overrides
Generates Claude Code local settings:
- AWS Bedrock configuration (if profile set)
- Anthropic API fallback
- Machine identification
The template system integrates with the bootstrap process:
- First bootstrap: Templates are not rendered (no
_variables.local.shyet) - After
blackdot template init: Bootstrap will render templates - Subsequent bootstraps: Templates re-render if configured
Bootstrap calls render_templates() which:
- Checks if
_variables.local.shexists - Renders all templates with
--force - Skips gracefully if not configured
blackdot doctor includes template system checks:
── Template System ──
✓ Template variables configured
✓ Found 4 generated config(s)
✓ All generated configs up to date
Warnings shown for:
- Missing
_variables.local.sh(info only) - No generated configs
- Stale templates (source newer than generated)
Check variables file exists:
ls -la templates/_variables.local.shIf missing, initialize:
blackdot template initCheck current values:
blackdot template varsCheck precedence:
DEBUG=1 blackdot template render --dry-runCheck status:
blackdot template diffForce re-render:
blackdot template render --forceTemplates warn about unresolved {{ variable }} placeholders. Check:
- Variable name spelling in template
- Variable defined in
_variables.shor_variables.local.sh - No typos in array key names
Check current detection:
blackdot template vars | grep machine_typeForce machine type:
# In _variables.local.sh
TMPL_AUTO[machine_type]="work"-
Always run
blackdot template varsafter editing_variables.local.shto verify values -
Use
--dry-runfirst when testing template changes -
Keep sensitive values out of templates - use vault system for secrets
-
Test on a fresh machine by temporarily renaming
_variables.local.sh -
Commit template changes, not generated files -
generated/is gitignored -
Document custom variables in your
_variables.local.shwith comments
Template variables can be stored in your vault for portable restoration across machines. This enables a seamless new-machine workflow.
The template system has built-in vault commands:
blackdot template vault push # Backup to vault
blackdot template vault pull # Restore from vault
blackdot template vault diff # Compare local vs vault
blackdot template vault sync # Bidirectional sync
blackdot template vault status # Show sync statusThe template vault commands sync templates/_variables.local.sh:
| File | Purpose |
|---|---|
templates/_variables.local.sh |
Your machine-specific template variables |
Vault: Template-Variables |
Backup in vault (all backends supported) |
# After running `blackdot template init`
blackdot template vault push # Push to vault
# Or check status first
blackdot template vault status # Shows sync state# 1. Pull template variables from vault
blackdot template vault pull
# 2. Render templates - uses variables from vault
blackdot template render
# 3. Create symlinks
blackdot template linkThe blackdot vault scan (discover-secrets.sh) command automatically detects template variables:
# Preview what would be discovered
blackdot vault scan --dry-run
# Output includes:
# [OK] Found: ~/.config/blackdot/template-variables.shTemplate variables are stored like any other syncable item:
{
"vault_items": {
"Template-Variables": {
"path": "~/.config/blackdot/template-variables.sh",
"required": false,
"type": "file"
}
},
"syncable_items": {
"Template-Variables": "~/.config/blackdot/template-variables.sh"
}
}With template variables in vault, setting up a new machine becomes:
# 1. Clone blackdot
git clone https://github.com/you/blackdot ~/.blackdot
# 2. Bootstrap (installs dependencies, sets up vault)
cd ~/.blackdot && ./install.sh
# 3. Pull SSH keys, AWS config, etc from vault
blackdot vault pull
# 4. Pull template variables from vault
blackdot template vault pull
# 5. Render all templates
blackdot template render
# 6. Create symlinks
blackdot template link
# Done! All configs are now in place- Main README - Overview
- Architecture - Complete guide
- Vault System - Secret management
- Architecture - System design