This document is the normative contributor standard for Base. It describes how code should be organized, where logic should live, and how Base-owned Bash, Python, Go, CLI, manifest, documentation, and test changes should behave.
For longer rationale, see:
Base is a layered developer-workspace tool. Contributors should preserve the layer boundaries rather than placing logic wherever it is easiest to call.
| Need | Owning layer |
|---|---|
| Public user command surface | bin/basectl and small real launchers under bin/ |
| Dispatch to Base subcommands | cli/bash/commands/basectl/basectl.sh |
| Host bootstrap such as Homebrew, Xcode CLT, Python, and venv creation | Bash setup layer |
| Shell runtime, prompt, activation, profile wiring, and dotfile guards | Bash runtime and shell layer |
| Manifest parsing and validation | Python layer |
| Artifact reconciliation, project discovery, project status, and structured project data | Python layer |
| Python package execution inside a project virtual environment | bin/base-wrapper |
| Project-owned Go CLIs and compiled binaries | Project repository, declared through manifest commands |
| Persistent Base state such as config and project venvs | ~/.base.d |
| Ephemeral logs, temp files, and cache | Base cache root, normally ~/Library/Caches/base on macOS |
$BASE_HOME/bin is the only public command surface that should be added to
PATH.
Public commands in bin/ should be real launcher files, not symlinks. Keep them
small and delegate into the command implementation. For a Bash command:
#!/usr/bin/env bash
exec "$(dirname "$0")/basectl" caff "$@"The implementation still belongs under:
cli/bash/commands/<command>/<command>.sh
Python CLIs should not expose #!/usr/bin/env python or a venv-specific Python
path as their public execution contract. If a Python CLI needs a shebang-bearing
public executable, use a small launcher that execs base-wrapper so the package
runs in the selected project virtual environment:
#!/usr/bin/env bash
exec "$(dirname "$0")/base-wrapper" --project "${BASE_PROJECT:-base}" example_cli "$@"This keeps direct CLI execution aligned with the same venv and PYTHONPATH
rules that basectl uses internally.
bin/basectl is the control plane. It decides whether the user asked to:
- run a Base command
- run an explicit Bash script path inside the Base runtime
- start an interactive Base runtime shell
bin/base-wrapper is the Python execution wrapper. It runs Python packages with:
BASE_HOMEset to the physical Base installationBASE_PROJECTset to the selected projectPYTHONPATHcontaining Base'slib/pythonandcli/python- Python resolved from
~/.base.d/<project>/.venv, unless explicitly overridden for tests
Use base-wrapper --project <project> <python-package> whenever Bash needs to
call Base's Python layer.
- Prefer simple, explicit code over clever dispatch.
- Keep changes scoped to the layer and module that own the behavior.
- Use structured APIs over ad hoc text parsing when a reasonable parser or data model exists.
- Keep stdout reserved for command output that users or automation may consume. Logs and diagnostics should go to stderr.
- Destructive operations must be dry-run by default or require an explicit
confirmation flag such as
--yes. - Error messages should explain what failed and what the user can do next.
- Add a new abstraction only when it removes real duplication or captures a stable product concept.
- Do not introduce hidden import-time side effects. Registration of CLI command functions is acceptable; filesystem, network, and process mutations are not.
-
Use four spaces for indentation. No tabs.
-
Shell/local variables and function names follow
snake_case. -
Reserve all-uppercase names for:
- exported environment variables
- constants
- globals intentionally shared across scripts, sourced modules, or subshells
-
Use a common prefix for exported environment variables whenever practical. For example:
BASE_HOME,BASE_HOST,BASE_OS,BASE_BASH_LIB_DIR. -
Do not use all-uppercase names for ordinary script-local variables.
-
Use a leading underscore for private variables and functions, especially in libraries or sourced modules where internal names might otherwise collide.
-
Avoid
camelCasein shell code. -
Place most code inside functions and invoke the main function at the bottom of the script.
-
Make sure all local variables inside functions are declared
local. -
Use
__func__naming convention for special-purpose variables and functions when a shared framework-level convention already exists. -
Double-quote all variable expansions, except:
- inside
[[ ]]or(( )) - places where word splitting is intentionally required
- inside
-
Use
[[ $var ]]to check ifvarhas non-zero length, instead of[[ -n $var ]]. -
Use compact control-flow formatting:
if condition; then ... fi while condition; do ... done for ((i = 0; i < limit; i++)); do ... done
-
Make sure shell code passes ShellCheck unless a documented exception is necessary.
Libraries should guard against repeated sourcing:
[[ -n "${_base_example_lib_sourced:-}" ]] && return
_base_example_lib_sourced=1
readonly _base_example_lib_sourcedPrefer module-specific guard names to generic names that could collide across sourced files.
- Do not use
set -e,set -u, orset -o pipefailin Base shell scripts or libraries. - Do not rely on implicit shell exit behavior for control flow.
- Prefer explicit error handling using helper functions such as:
runexit_if_errorfatal_error
- When a command may fail as part of normal flow, handle that failure with
if,case,||, or an explicit return-code check. - A script should make its error-handling strategy obvious to the reader.
Rationale:
set -e,set -u, andpipefailinteract poorly with conditionals, pipelines, subshells, sourced code, and scripts that are intended to be read as control-plane logic.- Base is a runtime- and library-heavy shell framework, so implicit exit rules make control flow harder to reason about.
- Explicit error handling is more verbose, but much easier to debug and maintain.
Bash owns bootstrap and runtime coordination. Bash should:
- install or verify host prerequisites
- create and validate virtual environments
- update shell startup files
- start runtime shells
- call Python packages through
base-wrapper
Bash should not:
- parse project manifests beyond passing manifest paths and project names
- reimplement artifact reconciliation that belongs in Python
- emit structured JSON by hand unless the format is tiny and well-tested
- call Python directly when
base-wrapperis the intended path
Base Python code follows PEP 8 style in spirit and the repo's existing patterns in practice.
- Use
from __future__ import annotationsin Python modules. - Prefer
pathlib.Pathover string path manipulation. - Use dataclasses for small structured records when they make behavior clearer.
- Prefer explicit return values over mutation-heavy helper APIs.
- Use
json.dumpsfor JSON output. Do not assemble JSON with string concatenation. - Keep module-level side effects limited to cheap constants and CLI command registration.
- Put CLI behavior in small command functions and pure helper functions where practical.
- Avoid broad exception catches. Catch the error type that represents the expected failure and convert it into a clear user-facing message.
Base Python CLIs should use base_cli.App:
from __future__ import annotations
import base_cli
import click
app = base_cli.App(name="example_cli")
def main(argv: list[str] | None = None) -> int:
try:
result = app.click_command.main(args=argv, standalone_mode=False)
except click.ClickException as exc:
exc.show()
return int(exc.exit_code)
return int(result or 0)
@app.command(context_settings={"help_option_names": ["-h", "--help"]})
@base_cli.option("--dry-run", is_flag=True, help="Preview changes without writing.")
def run(ctx: base_cli.Context, dry_run: bool) -> int:
ctx.log.info("Running example_cli.")
if dry_run:
print("[DRY-RUN] Would do the work.")
return 0
return 0Package entrypoints should provide __main__.py:
from .engine import main
raise SystemExit(main())- Use
ctx.logfor diagnostics. - Use stdout only for the command's primary output.
- JSON output must be deterministic and parseable.
- Redact sensitive option values using
base_cli.option(..., sensitive=True)when an option may carry credentials, tokens, or secrets. - Python CLI log files are runtime artifacts and should remain under the Base cache root with user-only permissions.
Python packages for Base commands live under:
cli/python/<package>/
Shared Python libraries live under:
lib/python/<package>/
Bash should invoke these packages with:
"$BASE_HOME/bin/base-wrapper" --project base base_projects listProject-specific Python commands should run through the project virtual environment:
: "${BASE_HOME:?BASE_HOME is required. Run through basectl activate <project>.}"
"$BASE_HOME/bin/base-wrapper" --project "$project" project_cli "$@"Do not hard-code ~/.base.d/base/.venv/bin/python in command implementations.
Do not use python -m <package> directly from Bash unless the code is a narrow
test fixture or bootstrap exception.
Base does not currently provide a Go CLI framework. For Go CLIs, Base should standardize expectations and orchestration first, then consider helper packages only after repeated real boilerplate appears.
Use Cobra (github.com/spf13/cobra) as the default framework for non-trivial Go
CLIs. Cobra is widely used, supports subcommands, flags, help, and completions,
and fits Base's expectation for professional command surfaces.
Tiny one-command tools may use the Go standard library when Cobra would add more structure than value. Once a tool has subcommands, completion needs, or shared flag behavior, prefer Cobra.
Base should not create a base-go-cli package until Banyan Labs or another real
project shows repeated Go CLI boilerplate. If such a package becomes useful, it
should wrap Base conventions around Cobra rather than replace Cobra.
Prefer conventional Go module layout:
cmd/<tool>/main.go
internal/<domain>/
internal/cli/
Keep main.go thin:
package main
import (
"os"
"example.com/project/internal/cli"
)
func main() {
os.Exit(cli.Run(os.Args[1:]))
}The command implementation should return an exit code instead of calling
os.Exit deep inside business logic.
Go CLIs should follow the same user-facing behavior standards as Base commands:
- Logs and diagnostics go to stderr.
- Primary command output goes to stdout.
- JSON output uses
encoding/json. - Destructive operations require
--yesor are dry-run by default. --dry-runprints what would change without changing state.- Exit codes follow Base conventions:
0success,1operational failure,2usage or configuration error. - User-facing errors should be plain English and actionable.
- Use
context.Contextfor operations that may need cancellation, timeouts, or request-scoped values. - Avoid panics for normal user or environment errors.
Go binaries are compiled executables. They do not use base-wrapper, and they
should not rely on Python virtual environments.
Base should orchestrate Go project commands through manifests:
test:
command: go test ./...
commands:
lint:
command: go vet ./...
build:
command: go build ./cmd/mytoolUse basectl test <project> and basectl run <project> <command> to invoke
those contracts. Let Go own Go modules, builds, and test execution; let Base own
workspace discovery, setup orchestration, and command delegation.
Typical validation for Go changes:
gofmt -w .
go test ./...
go vet ./...Base-owned Bash CLIs should live in per-command directories:
cli/bash/commands/
caff/
caff.sh
README.md
tests/
Umbrella commands such as basectl keep the entry script in the command
directory and place internal subcommand modules underneath:
cli/bash/commands/basectl/
basectl.sh
subcommands/
setup.sh
check.sh
tests/
basectl.bats
setup.bats
Bash libraries should live in per-library directories:
lib/bash/
std/
lib_std.sh
README.md
tests/
git/
lib_git.sh
README.md
tests/
Base Python command packages should live under cli/python. Shared Python
libraries should live under lib/python.
Keep package tests next to the package:
cli/python/base_projects/
engine.py
__main__.py
tests/
Small framework-level singleton files may remain flat when they are not modules in the same sense. Examples include:
bin/basectlbin/base-wrapperbase_init.shbootstrap.sh
Even though commands and libraries live in per-module directories, keep high-level index READMEs at parent levels when helpful, for example:
lib/bash/README.mdcli/bash/commands/README.md
Top-level READMEs should act as catalogs and maps. Local module READMEs should document the module itself.
- Help should be available through
-hand--helpwhen practical. - User-facing commands should return:
0for success1for operational failure2for usage or configuration errors
- Destructive commands must be dry-run by default or require
--yes. --dry-runshould print what would change without changing state.--format jsonshould be available when automation reasonably needs stable machine-readable output.- Text output should be readable, stable enough for humans, and not overly clever.
- Commands should keep logs on stderr and primary output on stdout.
- Commands that can run for a while should log progress at useful boundaries.
- Help paths and lightweight diagnostics should avoid requiring the Python venv when Bash can answer directly.
- Project manifests are declarative.
- The Python layer reads and validates manifests.
- Bash setup owns only bootstrap prerequisites needed before Python can run.
- Default project artifacts belong in
lib/base/default_manifest.yaml. - Developer prerequisites belong in
lib/base/dev_manifest.yaml. - Project-specific artifacts belong in the project's
base_manifest.yaml. - Prefer delegation to established tools such as Brewfile and mise instead of reimplementing their dependency models.
- Artifact setup should be idempotent. Running setup repeatedly should converge on the same state.
- Unknown artifact types or unsupported curated artifacts should fail clearly.
base_init.sh owns the runtime contract after bin/basectl chooses what should
run. It must be the single place that establishes convention-based Base paths
such as BASE_HOME, BASE_BIN_DIR, BASE_BASH_COMMANDS_DIR, and
BASE_BASH_LIB_DIR.
Bash scripts that run through Base should:
- define
mainas their entrypoint - keep ordinary code inside functions
- call
import_base_lib path/to/lib.shfor Base Bash libraries - rely on exported
BASE_*variables rather than reconstructing Base's repo layout locally
Shebang-based Bash scripts may use:
#!/usr/bin/env basectlIn that mode, basectl receives the script path as its first argument,
establishes the Base runtime, sources the script, and calls its main function.
Base-managed shell startup files follow this separation of concerns:
bash_profile/zprofile- thin login-shell behavior
bashrc/zshrc- interactive shell guards and dotfile-only behavior
- Base
bin/PATH availability for interactive shells
base_defaults.sh- optional shell-neutral interactive defaults shared by Bash and Zsh
bash_defaults.sh/zsh_defaults.sh- optional shell-specific interactive defaults
Startup files should stay thin and predictable. They must not source
base_init.sh; Base runtime setup belongs to the basectl command path.
~/.baserc is user-managed input for simple Base preferences such as
BASE_DEBUG=1. It must not set Base-owned runtime or profile state such as
BASE_HOME, BASE_BIN_DIR, BASE_LIB_DIR, BASE_OS, BASE_SHELL,
BASE_PROFILE_VERSION, BASE_ENABLE_BASH_DEFAULTS, or
BASE_ENABLE_ZSH_DEFAULTS. Shell startup code that sources ~/.baserc should
reject attempts to change those variables and restore the previous values.
- Prefer the narrowest test that proves the behavior.
- Use pytest for Python engines and helpers.
- Use BATS for Bash commands, runtime behavior, shell startup, and Bash libraries.
- Use
bin/base-testas the full confidence gate before merging broad or cross-layer changes. - Add regression coverage for bug fixes when practical.
- Avoid tests that depend on the user's real home directory, shell startup files, GitHub account state, or global config.
- Prefer fake commands and temporary repositories for shell integration tests.
- Keep test output deterministic.
Typical validation commands:
bats cli/bash/commands/basectl/tests/gh.bats
pytest cli/python/base_projects/tests
BASE_TEST_PYTHON="$HOME/.base.d/base/.venv/bin/python" \
env -u BASE_HOME HOME=/private/tmp/base-review-home \
bin/base-test
git diff --check- Update docs for user-visible behavior changes.
- Keep top-level README content focused on product usage and onboarding.
- Keep detailed design and rationale under
docs/. - Keep module-specific behavior in local module READMEs.
- Use GitHub default-style labels:
bugenhancementdocumentationcisecurityneeds-demo
- Issues created by Codex or other automation should be assigned to
codeforester. - Pull request work should happen in a dedicated worktree.
- Prefer
basectl ghwhen it supports the workflow. Fall back to rawgh, the GitHub connector, orgitwhen needed. - PR descriptions should include:
- what changed
- why it changed
- validation commands
Closes #<issue>orFixes #<issue>when appropriate- demo impact when relevant
Before adding code, ask where it belongs:
| Question | Put it here |
|---|---|
| Is this host bootstrap or shell runtime behavior? | Bash layer |
| Is this manifest parsing or project data? | Python layer |
| Is this artifact reconciliation? | Python layer, invoked by Bash through base-wrapper |
| Is this a public executable? | Small real launcher in bin/ |
| Is this a Bash helper used by multiple commands? | lib/bash/<module>/ |
| Is this a Python helper used by multiple CLIs? | lib/python/<package>/ |
| Is this a project-owned Go CLI? | Project Go module under cmd/<tool>/ and internal/, declared in base_manifest.yaml |
| Is this repeated Go CLI boilerplate? | Document the convention first; consider a shared Go helper only after real repetition |
| Is this command-specific behavior? | The command's module and tests |
| Is this a project-owned task? | base_manifest.yaml test or commands |
| Is this local machine preference? | ~/.base.d/config.yaml or ~/.baserc, depending on scope |
| Is this temporary runtime output? | Base cache root, not ~/.base.d |
When in doubt, preserve the layer boundary first and make the call path explicit.