Hash Devlog: Setting Up for Daily Driving

Building the infrastructure to use Hash as my daily shell: version injection, Homebrew distribution, and a quick way to report bugs from the terminal.

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:

  1. Push conventional commits to master (feat:, fix:, etc.)
  2. Release-please opens a PR that bumps the version and updates CHANGELOG
  3. Merge the PR → GitHub Release created with a tag
  4. 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, in 
    model.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.