\u0273Self
HomeMembershipCloudClawChatTaskDocs
GitHubGet Started

Product

  • Docs
  • Membership
  • Cloud
  • Changelog

Apps

  • ɳChat
  • ɳClaw
  • ɳTask

Community

  • GitHub
  • Discord
  • Blog

Legal

  • Privacy
  • Terms
ɳSelf

© 2026 ɳSelf. All rights reserved.

ɳSelfɳSELFCLI
DocsComparePricingChangelogBlogɳCloud
19★
All posts

Why we went Go: killing the 600-line bash script

April 12, 2026·nSelf Team·3 min read
engineeringgoarchitecture

The first version of nSelf was a single Bash script. It worked. You could run nself init, nself build, nself start, and get a full backend stack in minutes. We shipped it, people used it, and then reality set in.

The problems with Bash

Portability. macOS ships Bash 3.2 (from 2007) due to GPL licensing. Linux ships Bash 5. The syntax differences are subtle and painful. No associative arrays. No echo -e. No ${var,,} for case conversion. Every string operation needed a tr pipe or a printf workaround.

Testing. We used BATS (Bash Automated Testing System). It worked for simple cases, but testing complex flows (multi-service orchestration, config merging, error recovery) in Bash is like testing a house of cards in a wind tunnel.

Error handling. Bash has set -e and trap. That is the entire error handling toolkit. One missed error check and the script silently continues with corrupted state. We had a bug where a failed docker pull was swallowed, and docker compose up would start with a stale image. Users saw cryptic container errors with no connection to the actual problem.

Dependencies. The script shelled out to jq, yq, curl, openssl, docker, git, and more. Every user needed all of these installed. Every version mismatch was a new bug report.

Why Go

Single binary. go build produces one file. No runtime, no dependencies, no PATH issues. Users download it or brew install nself and it works.

Cross-compilation. GOOS=linux GOARCH=arm64 go build gives us an ARM Linux binary from a Mac. We ship binaries for macOS (Intel + Apple Silicon), Linux (x86_64 + ARM64), and Windows.

Concurrency. Service health checks, parallel container pulls, config file watching. Goroutines handle all of this cleanly. The Bash version ran health checks sequentially, which meant a 5-service stack took 25 seconds to verify. The Go version does it in 5.

Type safety. Config merging (.env.dev + .env.local + .env.secrets + .env.computed) was the single largest source of bugs in the Bash version. Typos in variable names silently produced wrong configs. Go structs with explicit fields catch this at compile time.

Testing. go test with table-driven tests. We went from 47 BATS tests to 1,900+ Go tests. Coverage went from "we think it works" to 82% line coverage with integration tests that spin up real Docker containers.

The rewrite

It took three months. We did not try to port the Bash logic line-by-line. Instead, we wrote a spec for every command, built the Go implementation from the spec, and verified behavior parity with integration tests.

The hardest part was the config system. The Bash version used source to load .env files, which meant every env file was executable code. The Go version parses env files as data, which is safer but required handling every edge case (quotes, multiline values, variable interpolation, comments) explicitly.

Results

MetricBashGo
Binary sizeN/A (script)14 MB
Dependencies12 external tools0
Test count471,900+
nself start (5 services)38s12s
Supported platformsmacOS + LinuxmacOS + Linux + Windows

The Go rewrite shipped as v1.0.0 on March 29, 2026. We have not looked back.

Get updates from the nSelf blog

Engineering posts, product updates, and technical guides. No spam.

PreviousThe nSelf plugin licensing model: how $0.99 beats SaaSNextSelf-hosted AI: why it matters

Related posts

ɳClaw: a personal AI with infinite memory

ɳClaw remembers everything. Not because it dumps chat logs into a vector store, but because it builds a structured knowledge graph from every conversation. Here is how it works.