I’ve been using Hash for development, but switching between go build outputs and keeping track of versions was getting old. If I’m going to use this as my daily shell, I need proper infrastructure: versioned builds, easy upgrades, and a fast way to report bugs when things break.
This devlog covers three pieces: version injection with dual VCS support, Homebrew distribution via a personal tap, and a new issue builtin that makes bug reports trivially easy.
Version Injection with Dual VCS
Hash lives in both jj and git. I want version output that reflects whichever VCS is relevant:
❯ hash --version hash 0.1.2 (jj:kpqxywz git:abc123f 2026-01-23)
The trick is ldflags. Go lets you inject values at build time:
var (
version = "dev"
gitCommit = "unknown"
jjChange = "unknown"
buildDate = "unknown"
)
Without ldflags, you get hash dev. With them, the build script fills in the real values:
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
JJ_CHANGE=$(jj log -r @ --no-graph -T 'change_id.shortest()' 2>/dev/null || echo "unknown")
BUILD_DATE=$(date -u +%Y-%m-%d)
LDFLAGS="-X main.version=${VERSION}"
LDFLAGS="${LDFLAGS} -X main.gitCommit=${GIT_COMMIT}"
LDFLAGS="${LDFLAGS} -X main.jjChange=${JJ_CHANGE}"
LDFLAGS="${LDFLAGS} -X main.buildDate=${BUILD_DATE}"
go build -ldflags "${LDFLAGS}" -o hash ./cmd/hash
The version output adapts based on what’s available. Local development builds show both VCS contexts. Release builds and Homebrew installs only have git commits; jj changesets don’t survive GitHub’s tarball process.
Homebrew Distribution
I want brew install hash to work. That means a personal tap and automated releases.
The setup:
- Main repo:
github.com/tfcace/hash - Tap repo:
github.com/tfcace/homebrew-hash
Users install with:
❯ brew tap tfcace/hash ❯ brew install hash
Release Automation
The workflow uses release-please to handle versioning:
- Push conventional commits to master (
feat:,fix:, etc.) - Release-please opens a PR that bumps the version and updates CHANGELOG
- Merge the PR → GitHub Release created with a tag
- Second job calculates the tarball SHA256 and updates the Homebrew formula
The formula itself builds from source with ldflags:
class Hash < Formula
desc "AI-powered shell with ACP integration"
homepage "https://github.com/tfcace/hash"
url "https://github.com/tfcace/hash/archive/refs/tags/v0.1.2.tar.gz"
sha256 "..."
license "MIT"
depends_on "go" => :build
def install
commit = `git rev-parse --short HEAD`.strip
ldflags = %W[
-X main.version=#{version}
-X main.gitCommit=#{commit}
-X main.jjChange=unknown
-X main.buildDate=#{Time.now.utc.strftime("%Y-%m-%d")}
]
system "go", "build", *std_go_args(ldflags:), "./cmd/hash"
end
end
No manual version bumps. No forgetting to update the formula. Conventional commits drive everything.
The issue Builtin
Here’s the feature I’m most excited about. When something breaks, I want to file a bug report without leaving the terminal.
❯ python train.py --epochs 100 Traceback (most recent call last): File "train.py", line 42, inmodel.fit(data) RuntimeError: CUDA out of memory ❯ issue "training crashes on large batch" # Opens editor with pre-filled template
The template pre-fills version info, OS, terminal, plus the last command’s stderr and exit code. I add context, save, and Hash submits via gh issue create. Exit without saving to abort, same pattern as git commit.
The !! Shortcut
Even faster: after a command fails, type !! to immediately open the issue editor with the failure context pre-filled:
❯ some-command --that-crashes Error: unexpected EOF ❯ !! # Opens issue editor with context from the failed command
If the last command succeeded, Hash prompts for confirmation—you probably don’t want to report a successful command as a bug.
Capturing Stderr
The tricky part is capturing stderr without breaking the normal flow. Hash wraps stderr in a capture buffer that stores up to 10KB while still writing to the terminal:
type stderrCapture struct {
original io.Writer
buf bytes.Buffer
mu sync.Mutex
}
func (c *stderrCapture) Write(p []byte) (n int, err error) {
c.mu.Lock()
if c.buf.Len() < maxStderrCapture {
remaining := maxStderrCapture - c.buf.Len()
if len(p) > remaining {
c.buf.Write(p[:remaining])
} else {
c.buf.Write(p)
}
}
c.mu.Unlock()
return c.original.Write(p)
}
After each command, Hash stores the stderr, exit code, command string, and working directory. The issue command and !! shortcut pull from this state.
What’s Next
This is the infrastructure for daily driving. With versioned builds, Homebrew distribution, and fast bug reporting, I can actually use Hash as my primary shell and iterate quickly when things break.
Hash is being developed on GitHub. If you try it and hit something weird, soon you’ll be able to just type !! and tell me about it.