Set up the developer toolchain
The project uses Go for the CLI and broker, plus native plugin toolchains and site-quality tools. On macOS, make config uses Homebrew; on Debian or Ubuntu it uses apt. It installs Go, Node.js/npm, Python, uv, Rust/Cargo, and the project’s lint and security tools. The repository’s Go module specifies the Go version expected by the build.
make config
Packaging macOS installers also uses Apple’s Xcode Command Line Tools. Packaging Linux .deb and .rpm artifacts requires Ruby, fpm, and RPM packaging utilities. The setup script installs the common prerequisites, but platform-specific packaging tools may still be needed for release builds.
For first-time setup details and contribution expectations, see CONTRIBUTING.md.
Everyday development workflow
Run the focused tests while iterating. Before pushing, the installed pre-push hook validates every exact outgoing commit in a clean temporary worktree. Run the same gates manually with:
make check-static
make check-security
make test
make config installs the repository-managed Git hooks. The local hook blocks pushes when checks fail, but can be bypassed with git push --no-verify; it is not a server-enforced status check.
Most Make targets use scripts/dev/run-quiet.sh: successful steps print one concise ok line, while failed steps print their captured output. Prefix a target with V=1 to stream its complete output as it runs:
make V=1 lint
make V=1 test
make V=1 verify
V=1 controls Make’s quiet wrapper. It is separate from ASH_VERBOSE, which enables runtime diagnostics in the Ash program.
Make target reference
make lint- Checks Go formatting and lint, shell scripts, HTML, tracked JSON, GitHub Actions workflows and YAML formatting, localization catalogs, Python, Markdown, and plugin sources.
make test- Builds and tests native plugins, then runs
go test ./.... make verify-ci- Runs the pre-push gate before scheduled nightly tag creation. Tag-triggered artifact CI no longer repeats source checks.
make verify- Runs the full local quality gate: lint, tests, race tests, coverage, vet, static analysis, security, secret scans, fuzzing, and benchmarks.
make test-race- Runs Go tests with the race detector.
make test-cover- Writes
coverage.outand checks the total againstCOVERAGE_MIN(60% by default). make test-fuzz- Runs application fuzz tests for
FUZZ_TIME(10 seconds by default). make security- Runs Go security and vulnerability analysis plus npm, Cargo, and Python dependency audits.
make pre-push- Runs static checks, security and dependency scanning, secret scanning for outgoing commits, and tests. The Git pre-push hook runs this against each pushed commit.
make build- Runs lint and tests, builds and installs native plugins, and builds
ashplusash-brokerunder~/.local/bin. make install- Runs tests, lint,
gosec, and plugin build/install, builds both binaries, then runsash install --shell bash --shell-integration --overwrite. Use it when you intend to update the local Bash integration. make plugins-build,plugins-test,plugins-lint- Build, test, or lint each plugin under
internal/app/ash_bootstrap/plugins_src.make plugins-cleanremoves the local plugin build output. make site-locales- Generates translated pages and refreshes the sitemap. Use
make site-locales SITE_LOCALES="zh-TW lb_LU"to select locales.
make all runs verify and then install. The complete target recipes and overridable variables are in the Makefile.
Plugin builders
Plugins are standalone executables with a language-independent contract. The plugin Makefiles select each plugin’s toolchain; local builds target the host by default. Release builds pass PLUGIN_TARGET_GOOS and PLUGIN_TARGET_GOARCH so Rust and other non-Go plugin binaries match the release artifact.
The top-level builder excludes plugins with a Cargo.toml from macOS targets, including native macOS builds. macOS bundles what_time_is_it and calculator; Linux and FreeBSD also bundle the Rust flip_a_coin plugin. Rust source tests remain enabled on developer machines.
Use make plugins-build to build them into bin/plugins, make plugins-test to run their tests, and make plugins-install to copy them into the user plugin directory. See the native plugin contract before adding or changing a plugin.
Release builders and artifacts
Source checks run locally before pushes. Tag-triggered artifact jobs build and validate the release outputs without repeating those checks. Linux packaging jobs build both macOS architectures without Rust plugins and publish tarballs for Homebrew; no macOS runners or CI-generated .pkg installers are used. Linux builders cover amd64 and arm64 and validate .deb, .rpm, and tarball contents. FreeBSD is cross-built and packaged for amd64.
Go release binaries are built with trim-path and stripped linker options. Included native plugins match the target OS and architecture. CI checks macOS archive contents and Mach-O architectures but cannot execute macOS binaries on Linux. Linux and FreeBSD tarballs retain runtime smoke tests; the workflow does not install packages or configure fresh shell integrations.
For local artifact builds, specify the target OS, architecture, and version explicitly:
make release-artifacts \
RELEASE_TARGET_OSES=linux \
RELEASE_TARGET_ARCHES="arm64 amd64" \
RELEASE_VERSION=v1.2.3
release-artifacts builds and validates the formats supported for that target. The defaults are host OS (auto) and both amd64 arm64; FreeBSD publishing is limited to amd64. Artifacts go under dist/release. For a single binary/plugin build, use make release-build-one RELEASE_GOOS=linux RELEASE_ARCH=arm64 RELEASE_VERSION=v1.2.3.
For macOS tarballs built on Linux, use make release-artifacts RELEASE_TARGET_OSES=darwin RELEASE_TARGET_ARCHES="amd64 arm64" RELEASE_MACOS_PKG=0 RELEASE_VERSION=v1.2.3. Local macOS packaging still defaults to generating a .pkg and requires Apple packaging tools. Local release outputs are not handed off to CI.
make version runs release checks and builds artifacts without publishing. A normal make release requires macOS, a clean working tree and configured Git identity, generates release notes, creates/pushes release references, and watches the GitHub workflow. Scheduled nightly orchestration runs on Ubuntu with RELEASE_SKIP_LOCAL_ARTIFACTS=1 after its local gate, then the tag workflow builds artifacts. The default channel is beta; select stable or nightly with CHANNEL=stable or CHANNEL=nightly.
Read the release workflow and release secrets guide before publishing.
Verbose runtime logging
ASH_VERBOSE enables structured JSON debug logging and prints an execution dashboard before Ash exits. Accepted values are 1, y, yes, true, on, and debug, case-insensitively. The dashboard summarizes stage timings, tool-call count, token totals when available, and total realtime.
ASH_VERBOSE=1 ash "investigate this error"
ASH_VERBOSE=1 \
ASH_LOG_FILE="$HOME/.local/state/ash/debug.jsonl" \
ASH_LOG_MAX_BYTES=10485760 \
ash "investigate this error"
When ASH_LOG_FILE is set, verbose JSON log records are written to that path; the parent directory is created as needed. ASH_LOG_MAX_BYTES controls rotation and defaults to 1048576 bytes (1 MiB). Without an explicit log file, debug output uses the process diagnostic writer. Keep logs private and review them before sharing.
This is distinct from Make’s V=1: use V=1 to see build/test command output and ASH_VERBOSE=1 to diagnose an Ash runtime invocation.
Find the source of a log record
Every Ash logging call has a unique EID field. Copy the EID from the JSON record and search for its literal in the Go source to find the call site:
rg '"EID", "EpbG2YtZ"' internal/
For example, a source call records the message and the EID together with structured context. EIDs are stable identifiers for log locations: do not translate, rename, or reuse them. The debug logger and its rotation behavior are implemented in internal/app/debuglog.go.
Test native package installs with vm.sh
scripts/vm.sh detects the host OS and selects a VM backend: Tart on Apple Silicon macOS, or QEMU/KVM on x86_64 Linux. Both backends can test APT on Ubuntu/Debian and DNF on Fedora/Rocky Linux. Tart on macOS can also test the macOS Homebrew package; Linux hosts cannot run macOS guests and therefore cannot test that path.
macOS host prerequisites
Use an Apple Silicon Mac running macOS 13 or later, with Tart installed, network access to the VM image and Ash repository, and enough disk space for the VM. Install Tart with Homebrew if needed:
brew install openai/tools/tart
Linux host prerequisites
Use an x86_64 Linux host with hardware virtualization enabled and a user account that can read and write /dev/kvm (commonly by joining the kvm group and logging in again). Install QEMU/KVM, cloud-image-utils for cloud-localds, curl, OpenSSH client tools, and Python 3. Example package commands:
# Debian or Ubuntu
sudo apt-get install qemu-system-x86 qemu-utils cloud-image-utils curl openssh-client python3
# Fedora or Rocky Linux
sudo dnf install qemu-kvm qemu-img cloud-utils curl openssh-clients python3
The Linux backend launches x86_64 Ubuntu, Debian, Fedora, or Rocky cloud images with QEMU/KVM and cloud-init. It uses QEMU user-mode networking with SSH forwarded to localhost; no libvirt network setup is needed. It does not run macOS guests, does not test Homebrew on macOS, and currently requires an x86_64 Linux host. Arch and FreeBSD are not supported targets.
Run a package test
Automated mode is the default and removes the VM after the test. The default platform follows the host: macOS selects macos, Linux selects ubuntu. Choose another supported platform and mode explicitly. The package channel defaults to beta; use --channel nightly, --channel beta, or --channel release to select one. --channel stable is an alias for release.
scripts/vm.sh --platform macos --mode interactive
scripts/vm.sh --platform ubuntu --channel nightly --mode automated
scripts/vm.sh --platform debian --mode interactive
scripts/vm.sh --platform fedora --mode automated
scripts/vm.sh --platform rocky --mode interactive
Interactive mode leaves the installed VM running for manual testing. On macOS, run the printed tart exec -it command in a terminal to open a guest shell. Exiting that shell leaves the VM running; shut down the guest or stop it from the host. To stop and delete a retained Tart VM, use:
tart stop VM_NAME && tart delete VM_NAME
On Linux, the script prints an SSH command and a stop command; stop the VM and remove its state with:
scripts/vm.sh --stop VM_NAME
Automated mode runs installation and shell-setup smoke checks, then stops and removes the VM. Downloaded base images remain cached. Tart images can be changed with ASH_TEST_VM_IMAGE; Linux cloud-image URLs can be overridden with ASH_TEST_QEMU_IMAGE. Linux images are cached under ${XDG_CACHE_HOME:-~/.cache}/ash/vm-images, and interactive VM disks and SSH keys are kept under ${XDG_STATE_HOME:-~/.local/state}/ash/vm. Set ASH_TEST_VM_CACHE_DIR or ASH_TEST_VM_STATE_DIR to change those locations.
To let the guest connect to the same AI service as the host, export the needed ASH_AI_* variables before running the script, for example ASH_AI_ENDPOINT, ASH_AI_MODEL, and, when needed, ASH_AI_AUTH_TOKEN. Exported values are forwarded to the guest; unexported shell variables are not. Interactive VMs retain these credentials in the guest profile with mode 0600; Linux VM disks are not encrypted, so protect the state directory and stop/remove the VM when finished.
See scripts/vm.sh --help for the current backend prerequisites, supported platforms, and options.
Project source guides
- Contributing guide — setup, tests, release expectations, and CI notes.
- Architecture — project layout and runtime subsystems.
- README — product, install, configuration, and environment reference.
- Makefile — authoritative target recipes and build variables.
- GitHub workflows — CI, platform builders, and publishing automation.