Skip to content

🀝 Contributing to ZSHAND¢

πŸ“‹ This guide covers how to get going (cloner first), when to use config vs the repo, conventions, workflow, and quality requirements for contributing.

🧠 Philosophy

Every contribution should leave the codebase faster, better documented, and better tested than before. Headers are not optional β€” they're how we communicate intent to humans, AI, and tooling alike.


πŸš€ Quick start: use the installer (cloner)ΒΆ

The recommended way to get a working ZSHAND environment is the cloner. It installs the framework, sets up your config directory, and runs the platform installer so you have a working shell and (optionally) dev tools.

Run the clonerΒΆ

bash <(curl -fsSL https://raw.githubusercontent.com/presempathy/zshand/main/cloner.sh)

The cloner will: check prerequisites (git, curl, gum), detect your OS (Debian, Ubuntu, Manjaro, macOS, WSL), prompt for install path (default ~/code/zshand), clone the repo, run setup.zsh (creates ~/.config/zshand/, installs packages, compiles bundles), and offer to set up ~/.zshenv (e.g. ZSHAND_AUTO_FULL=1). After that, open a new terminal or exec zsh.

Cloner optionsΒΆ

Option Purpose
--dry-run Show what would be done, no changes
--non-interactive No prompts (CI / scripting)
--update Update an existing clone (pull + setup)
--uninstall Remove symlinks and optional cleanup
--quiet Less output
--help Full list

Example: bash <(curl -fsSL .../cloner.sh) --dry-run to preview.

The installer (cloner + setup) is what gives you a working environment.


πŸ“‹ Prerequisites (what the installer uses)ΒΆ

If you use the cloner (recommended), it and setup.zsh handle OS detection, package installs, and config creation. You only need git and curl to run the cloner; the installer will install gum if missing and run the platform script (Debian, Ubuntu, Manjaro, macOS, WSL).

For local development (running just test, just lint, contributing patches), you need these tools. The Dev Container installs them via mise; otherwise install mise and run mise install in the repo.

Tool Purpose
zsh 5.0+ Shell runtime
git 2.20+ Version control
just Task runner (just test, just lint)
ShellSpec BDD test framework
Trunk Linting (ShellCheck, shfmt, etc.)
mise Optional; installs just, shellspec, etc.
Hyperfine Optional; for benchmarking

See Installers for how the cloner and setup work.


πŸ“ Config vs main repo: where to add thingsΒΆ

Use your config directory when it’s only for you or experimental.
Contribute to the main repo when it’s a bug fix, feature for everyone, or new framework piece.

Add in ~/.config/zshand/ Add in main repo (PR)
Your own functions, aliases, widgets Bug fixes in framework code
Your own hooks (e.g. 60_my_tool.zsh) New features used by many users
Overrides (e.g. rc.d/14_aliases.zsh) New core modules, hooks, or bin tools
Personal init.d/ / post.d/ scripts Docs, tests, tooling improvements
Keybindings in keybindings.toml Performance or compatibility improvements

Why config first? Framework updates never overwrite your config; you can try ideas without forking. Same basename in config overrides the framework file.

When to move to the repo? When the change is generally useful, fits the existing structure, and you’re willing to maintain tests and docs (headers, spec file). See Config for the full layout.


πŸ“ ConventionsΒΆ

πŸ“ Header standardΒΆ

Every framework file must have a documentation header per HEADER_STANDARD.md. No exceptions.

Location Tier Title format
build/*.zsh (complex) Full ╔═╗ box
core/, functions/, hooks/, bin/ Standard ── name β€” Description ──
shared_functions/ Compact ── name β€” Description ──
config/*.conf, .zshand-deps Minimal Short # block

Checklist: Title Β· USAGE Β· DEPENDENCIES Β· EXIT CODES Β· EXAMPLES Β· TESTING (spec path) Β· SEE ALSO Β· 78‑char dividers.

πŸ”’ File namingΒΆ

Directory Pattern Example
core/ NN_name.zsh (even numbers) 08_audit.zsh
shared_functions/, startup/, hooks/ NN_name.zsh 01_stderr_error.zsh, 24_az_safe_mode.zsh
functions/, widgets/ name.zsh (no number) dcopy.zsh, aicmit.zsh
bin/ name (no extension) aicontext
private_functions/ _name.zsh _zshand_network_monitor.zsh

Numbering: Even = framework. Odd = user insertion (e.g. rc.d/03_my.zsh between 02_vars and 04_path).

🏷️ Naming and behavior¢

Scope Convention Example
Public (user-facing) Short, memorable zcheck, dcopy, zprime
Private (framework) _az_ or _zshand_ prefix _az_safe_mode_enter
Shared utilities Descriptive, lowercase stderr_error, load_file_timed

πŸ“‹ Other conventionsΒΆ

  • TOML for framework knobs β€” Use config.toml for dev_mode, quiet, perf_budget, keybindings; don’t invent new env vars for behavior. Config
  • Secrets only in secrets.zsh β€” Never in env.zsh or committed code. SECURITY
  • Load order β€” New core/ or hooks/ use the next free even number in the right range. ARCHITECTURE
  • Overrides β€” Same basename in config replaces framework; different name = both load. CORE

πŸ› οΈ Dev setup (for hacking on the framework)ΒΆ

If you’re contributing code to the repo, you need a dev environment. The cloner or Dev Container gets you most of the way.

Option 1: Dev Container (recommended) β€” Open repo in VS Code/Cursor with Dev Containers, click Reopen in Container. The container installs zsh, mise, and project tools. See 🐳 Using Dev Containers.

Option 2: Local (after cloner or manual clone)

# If you didn’t use the cloner:
git clone https://github.com/presempathy/zshand.git ~/code/zshand
cd ~/code/zshand
zsh setup.zsh
mise install

export ZSHAND_DEV_MODE=1
exec zsh

just test
just lint
zcheck

Dev mode

With ZSHAND_DEV_MODE=1, the framework loads individual files instead of bundles. Use it while developing so changes apply without recompiling.


πŸ“‹ Using Just (Task Runner)ΒΆ

Just is the project's task runner. It provides a consistent interface for common development tasks.

πŸš€ Quick StartΒΆ

# List all available tasks
just --list

# Run a task
just test
just lint
just build

πŸ“ Common TasksΒΆ

Task Description Usage
just test Run all ShellSpec tests just test or just test spec/path/
just lint Run all linters (Trunk) just lint
just format Auto-format code just format
just build Rebuild all bundles just build
just check Run full quality gate just check

⚠️ Important Notes¢

  • Always run just test before committing to ensure tests pass
  • Run just lint to catch formatting and linting issues early
  • Use just check before opening a PR to run the full quality gate
  • If a task fails, fix the errors before proceeding β€” don't skip tests or linting

Don't break the build

Never commit code that fails just test or just lint. These checks run in CI/CD and will block your PR. If tests are failing, fix them locally first.


🐳 Using Dev Containers¢

Dev Containers provide a consistent development environment across all contributors, ensuring everyone has the same tools and dependencies.

πŸš€ Quick StartΒΆ

  1. Open in VS Code/Cursor with the Dev Containers extension installed
  2. Click "Reopen in Container" when prompted, or use Command Palette:
  3. F1 β†’ Dev Containers: Reopen in Container
  4. Wait for setup β€” the container will build and install dependencies automatically

πŸ“¦ What's IncludedΒΆ

The dev container automatically installs:

  • zsh and zshdb (for debugging)
  • mise (tool version manager)
  • All development tools via mise (node, go, rust, uv, shellspec, just)
  • Git and GitHub CLI

πŸ”§ ConfigurationΒΆ

The dev container configuration is in .devcontainer/devcontainer.json:

  • Base image: mcr.microsoft.com/devcontainers/base:ubuntu
  • Features: Common utilities, Git, Python
  • Post-create: Installs zsh and zshdb

πŸ› Debugging in Dev ContainerΒΆ

The dev container includes zshdb for debugging zsh scripts:

  1. Set breakpoints in your .zsh files
  2. Use VS Code's Debug panel (F5)
  3. Select "Debug Zsh Script" configuration
  4. Step through code with full debugging support

⚠️ Important Notes¢

  • All development happens inside the container β€” your local files are mounted
  • Changes persist β€” files are synced between container and host
  • Run just test inside the container to ensure compatibility
  • Don't install tools manually β€” use mise or the container's package manager

First-time setup

The first time you open the dev container, it may take a few minutes to build. Subsequent opens are much faster thanks to Docker layer caching.


πŸ§ͺ Using ShellSpecΒΆ

ShellSpec is the BDD-style testing framework used for all test suites. It provides descriptive test syntax and comprehensive assertion matchers.

πŸš€ Quick StartΒΆ

# Run all tests
shellspec

# Run specific spec file
shellspec spec/functions/dcopy_spec.sh

# Run tests in a directory
shellspec spec/core/

# Run with verbose output
shellspec --format documentation

# Run with coverage (requires kcov)
shellspec --kcov

πŸ“ Writing ShellSpec TestsΒΆ

Basic StructureΒΆ

#!/usr/bin/env shellspec
# ── function_name_spec β€” Tests for function_name ──────────────────────────

Describe "function_name"
  # Setup (runs before each test)
  BeforeAll 'source "${PROJECT_ROOT}/functions/function_name.zsh"'

  It "returns success on valid input"
    When call function_name "valid_arg"
    The status should be success
    The output should include "expected text"
  End

  It "exits with error when argument missing"
    When call function_name
    The status should be failure
    The stderr should include "ERROR:"
  End

  It "handles edge cases correctly"
    When call function_name ""
    The status should be failure
  End
End

Common MatchersΒΆ

Matcher Purpose Example
The status should be success Check exit code 0 The status should be success
The status should be failure Check non-zero exit The status should be failure
The output should include Check stdout contains The output should include "text"
The stderr should include Check stderr contains The stderr should include "ERROR"
The output should eq Exact match The output should eq "exact"
The path should exist File exists The path "file.txt" should exist

Test OrganizationΒΆ

spec/
β”œβ”€β”€ functions/
β”‚   └── dcopy_spec.sh          # Tests for functions/dcopy.zsh
β”œβ”€β”€ core/
β”‚   └── 08_audit_spec.sh        # Tests for core/08_audit.zsh
└── shared_functions/
    └── 01_stderr_error_spec.sh # Tests for shared_functions/01_stderr_error.zsh

Rule: Spec files mirror the source tree structure.

⚠️ Important Notes¢

  • Always write tests for new functions or behavior changes
  • Run tests before committing β€” use just test or shellspec
  • Keep tests fast β€” avoid slow operations (network, file I/O) unless testing them
  • Test edge cases β€” empty strings, missing args, invalid input
  • Use descriptive test names β€” It "does something specific" not It "works"

Don't skip tests

All tests must pass before committing. If a test fails: 1. Read the error message carefully 2. Fix the code (not the test, unless the test is wrong) 3. Re-run the test to verify the fix 4. Run the full suite with just test before committing

πŸ“Š Coverage ExpectationsΒΆ

Change Type Minimum Coverage
New function βœ… Happy path + ⬜ error paths + ⬜ edge cases
Bug fix βœ… Regression test proving the fix
Refactor βœ… Existing tests still pass (no weakening)
Performance βœ… Behavioral tests + ⚑ benchmark comparison

πŸ”„ WorkflowΒΆ

🌿 Branch Strategy¢

main              ← stable, always passes tests
  └── feat/name   ← feature branches (short-lived)
  └── fix/name    ← bug fix branches
  └── docs/name   ← documentation-only changes

βœ… Before SubmittingΒΆ

Run the full quality gate:

# 1. Lint (fixes formatting automatically)
just lint                    # or: trunk check

# 2. Test (all tests must pass)
just test                    # Full ShellSpec suite
shellspec spec/core/         # Specific directory

# 3. Verify headers
grep -c '# ──' <your-file>  # Count section dividers

# 4. Benchmark (if touching startup path)
hyperfine --warmup 3 'zsh -ic exit'

# 5. Compile and verify
zprime                       # Rebuild all bundles
zcheck                       # Framework integrity

# Or run everything at once:
just check                   # Runs lint + test + build

Don't break the build

All tests (just test) and linters (just lint) must pass before submitting. These checks run automatically in CI/CD and will block your PR if they fail.

πŸ“ Commit MessagesΒΆ

Follow conventional commit format:

type(scope): short description

Optional longer description explaining why.

Closes #123
Type When
feat New feature or function
fix Bug fix
docs Documentation only
perf Performance improvement
refactor Code restructure (no behavior change)
test Adding or updating tests
chore Build system, deps, tooling

πŸ§ͺ Testing RequirementsΒΆ

Every contribution that adds or modifies behavior must include tests using ShellSpec.

See ShellSpec section above

For detailed information on writing and running ShellSpec tests, see the πŸ§ͺ Using ShellSpec section above.

πŸ“ Where Tests GoΒΆ

Spec files mirror the source tree:

functions/dcopy.zsh           β†’  spec/functions/dcopy_spec.sh
core/08_audit.zsh             β†’  spec/core/08_audit_spec.sh
shared_functions/01_stderr_error.zsh  β†’  spec/shared_functions/01_stderr_error_spec.sh

πŸš€ Running TestsΒΆ

# Run all tests (recommended)
just test

# Run specific spec file
shellspec spec/functions/dcopy_spec.sh

# Run tests in a directory
shellspec spec/core/

# Run with verbose output
shellspec --format documentation

πŸ“Š Coverage ExpectationsΒΆ

Change Type Minimum Coverage
New function βœ… Happy path + ⬜ error paths + ⬜ edge cases
Bug fix βœ… Regression test proving the fix
Refactor βœ… Existing tests still pass (no weakening)
Performance βœ… Behavioral tests + ⚑ benchmark comparison

Never weaken tests

Do not delete or relax existing test assertions without explicit approval. If a test needs updating, explain why in the commit message.

Tests must pass before committing

Always run just test before committing. Failing tests will block your PR in CI/CD.


✨ Linting & Formatting¢

The framework uses Trunk to manage all linters:

Linter Checks Config
🐚 ShellCheck Shell script correctness .trunk/configs/.shellcheckrc
πŸ“ shfmt Shell formatting .editorconfig
πŸ“ markdownlint Markdown formatting .trunk/configs/.markdownlint.yaml
πŸ”‘ gitleaks Secret detection Trunk default
πŸ“‹ cspell Spelling .cspell.json
πŸ“Š prettier Markdown/JSON formatting .prettierrc.yaml

πŸš€ Running LintersΒΆ

trunk check                  # Check all changed files
trunk check --all            # Check everything
trunk check <file>           # Check specific file
trunk fmt                    # Auto-fix formatting issues

⚠️ ShellCheck Overrides¢

If you must disable a ShellCheck rule:

  1. Always add a comment explaining why
  2. Prefer per-line disables over per-file
  3. If the same disable appears in 3+ files, add it to .shellcheckrc instead
# shellcheck disable=SC2034  β€” Variable exported for use by sourced modules
local MY_VAR="value"

⚑ Performance guidelines¢

The framework has strict performance targets. Profiling shows where time is spent so you can stay under budget.

Startup timeline example
Example startup timeline β€” what zprofile and the profile report help you achieve

The framework has strict performance targets:

Metric Target Measured By
⚑ Cold start <50ms hyperfine 'zsh -ic exit'
⚑ Warm start (P10k) <10ms Instant prompt to interactive
πŸ“Š Per-file budget <100ms ZSHAND_PERF_BUDGET enforcement

πŸ“ RulesΒΆ

  • 🚫 No network calls in startup path (hooks can defer)
  • 🚫 No subprocess spawning unless absolutely necessary
  • βœ… Use zsh builtins over external commands where possible
  • βœ… Lazy-load heavy integrations (Docker, cloud CLIs)
  • βœ… Cache results that don't change per-session
  • βœ… Profile before and after with ZSHAND_PROFILE_BUNDLE=1

πŸ“Š Benchmarking a ChangeΒΆ

# Before your change
hyperfine --warmup 3 'zsh -ic exit' --export-json before.json

# Make your change, rebuild
zprime

# After your change
hyperfine --warmup 3 'zsh -ic exit' --export-json after.json

# Compare (manual or via script)

πŸ“ Adding a New FileΒΆ

Step-by-stepΒΆ

  1. Choose the right directory (see ARCHITECTURE.md)
  2. Pick the correct number prefix (even = framework, odd = user slot)
  3. Write the header per HEADER_STANDARD.md
  4. Implement the functionality
  5. Write tests in the mirrored spec location
  6. Run the quality gate (lint, test, benchmark)
  7. Rebuild bundles with zprime
  8. Verify with zcheck

πŸ—‚οΈ Example: Adding a new functionΒΆ

# 1. Create the function
vi functions/my_function.zsh

# 2. Write header (Standard tier for functions/)
#    Include: USAGE, DEPENDENCIES, EXIT CODES, EXAMPLES, TESTING, SEE ALSO

# 3. Create the spec
vi spec/functions/my_function_spec.sh

# 4. Verify
just test spec/functions/my_function_spec.sh
trunk check functions/my_function.zsh
zprime

Document Purpose
πŸ“₯ INSTALLERS.md Cloner and setup (how the installer works)
πŸ—οΈ ARCHITECTURE.md Framework structure and boot sequence
πŸ“ HEADER_STANDARD.md Documentation header conventions
πŸ§ͺ TESTING.md Detailed testing guide
⚑ PERFORMANCE.md Performance profiling and optimization
🎨 STYLE_GUIDE.md Code style conventions
πŸ“š Documentation Index Full doc nav