Files
x/README.md
T
sneak 7feb212d04
check / check (push) Waiting to run
Adopt the scripts-to-rule-them-all layout and the canonical Docker gates
script/ holds the entrypoints and the Makefile targets shim to them.
Linting and testing are phases of the Dockerfile, built alone by
script/lint and script/test with a fresh CACHEBUST value and cache
mounts for Go's build cache and golangci-lint's; the build stage
depends on both, and the runtime stage ships the binary and the source
without .git. Formatting stays on the host in script/fmt and
script/fmt-check (gofmt and the prettier pinned in yarn.lock).
script/cibuild bootstraps, runs script/check and builds the tagged
image, and the Gitea workflow calls it. script/precommit refuses a
commit whose go.mod or go.sum is not tidy.

cgo stays off, so the test phase runs on the alpine Go image without
-race; that is the one departure from the canonical Go Dockerfile.

Re-vendored from the prompts repository's next branch: REPO_POLICIES.md
(2026-11-04), .golangci.yml (byte-identical, so the local
exhaustruct_v5 addition is gone now that canonical carries it),
.gitea/workflows/check.yml, .editorconfig, .gitignore and .dockerignore,
the last two followed by this repository's own entries. AGENTS.md holds
the repository-specific guidance for agents. The README gains the
Entrypoints section and describes the new gates.
2026-11-04 07:31:59 -08:00

12 KiB

x

Description

sneak.berlin/go/x is a WTFPL-licensed Go module and CLI tool by @sneak that serves as an umbrella publishing space for small reusable packages, plus sutil, a command-line utility holding a few small tools worth having on every machine.

Importable packages live in pkg/. The CLI is cmd/sutil/, and the code that exists only to support it lives in internal/.

Getting Started

Install the CLI

go install sneak.berlin/go/x/cmd/sutil@latest

Or, from a checkout:

make build      # builds ./sutil with the git version embedded
make install    # installs sutil into $(go env GOBIN)

Use the CLI

# An endless stream of fast pseudorandom bytes; stop it with Ctrl-C.
sutil generate random | pv > /dev/null

# The same, but deterministic: the same seed always produces the same bytes.
sutil generate random --count 1073741824 "my seed" > testdata.bin

# A memorable password: three dictionary words, each followed by a digit,
# joined by a randomly chosen delimiter. Requires a system word list.
sutil generate password
# marble7-sprout2-velvet9

# Build metadata, including which config file (if any) was loaded.
sutil version
sutil info

# Every command first loads ~/.sneak.yaml (or .json, .toml, ...) if it
# exists, or the file named with --config.
sutil --config ./sneak.yaml info

Use the packages

package main

import (
    "fmt"

    "sneak.berlin/go/x/pkg/bip39wordlist"
    "sneak.berlin/go/x/pkg/cbk"
)

func main() {
    mnemonic, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
    if err != nil {
        panic(err)
    }

    // The same mnemonic always yields the same keypair, on any machine.
    // The keys are plain crypto_box keys for use with
    // golang.org/x/crypto/nacl/box.
    keypair, err := cbk.New(cbk.Params{Mnemonic: mnemonic})
    if err != nil {
        panic(err)
    }

    fmt.Println(keypair.PublicKeyHex())
}

Rationale

  • Reusable odds and ends need a home. Several small packages are worth sharing between projects but are too small to each deserve a repository, CI pipeline, and release cadence of their own. This module collects them under one import path.
  • /dev/urandom is slow. Operating system random devices are tuned for small requests of key material, not for filling a disk or producing gigabytes of test data. sutil generate random streams pseudorandom bytes at close to memory bandwidth, and can do so deterministically from a seed so test data is reproducible.
  • Keys you can carry in your head. A crypto_box keypair derived from a BIP39 mnemonic can be regenerated anywhere from a phrase a human can write down or memorize, with no key file to lose. pkg/cbk makes that derivation explicit and reproducible.
  • Passwords you can type. Three dictionary words with digits and a delimiter are far easier to transcribe than line noise, and with the macOS word list they still carry about 62 bits of entropy.

Design

Layout

Path Purpose
cmd/sutil/ package main; the whole entrypoint is a few lines
internal/cli/ The cobra command tree, built by cli.New
internal/randstream/ Fast seeded pseudorandom byte stream
internal/pwgen/ Password generator over the system dictionary
pkg/bip39wordlist/ BIP39 English wordlist and mnemonic helpers
pkg/cbk/ crypto_box keypairs derived from BIP39 mnemonics

The split between pkg/ and internal/ is deliberate and should be kept: pkg/ holds only packages designed to be imported by other programs, with a stable, documented API. Anything that exists to support sutil goes under internal/, even if it would be harmless to export, so that the public surface of the module stays small and intentional.

The command tree holds no global state: cli.New builds every command from a Params struct, so tests construct it repeatedly with captured input and output, and nothing happens in init functions. Before any command runs, the root command's persistent pre-run loads the optional configuration file (the one named by --config, or else .sneak.* in the home directory found with go-homedir) into a viper instance that also exposes environment variables. No command consumes settings from it yet; sutil info reports which file was loaded. Tests point Params.ConfigDir at a temporary directory so the real home directory is never read.

pkg/cbk

Derives NaCl crypto_box keypairs (Curve25519) deterministically from BIP39 mnemonics, and nothing else. The entropy comes from BIP85 through sneak.berlin/go/secret/pkg/bip85, so any BIP85-capable tool given the same mnemonic reproduces the same key, and one mnemonic can back any number of independent keypairs:

  1. The mnemonic is validated and canonicalized with bip39wordlist.NormalizeWordlist.
  2. The standard 64-byte BIP39 seed is computed with PBKDF2-HMAC-SHA512 (2048 rounds, salt "mnemonic"), exactly as a BIP39 wallet would with an empty passphrase. BIP39 passphrases are deliberately not supported.
  3. The seed becomes a BIP32 master key.
  4. BIP85's HEX application derives 32 bytes of entropy at m/83696968'/128169'/32'/{index}'; those bytes are the private key.
  5. The public key is the Curve25519 base-point multiple of the private key.

The resulting keys are plain 32-byte crypto_box keys for use with golang.org/x/crypto/nacl/box. The API:

  • cbk.New(cbk.Params{Mnemonic, Index}) and cbk.FromMnemonic(m) (index 0) return a CryptoBoxKeypair.
  • CryptoBoxKey.Hex(), CryptoBoxKey.Bytes(), and the PublicKeyHex, PrivateKeyHex, PublicKeyBytes, PrivateKeyBytes accessors on the keypair.

The test suite checks the mnemonic-to-master-key step and the BIP85 HEX application against the BIP85 specification's own test vectors, verifies that the private key is exactly the BIP85 output, and pins the keypair for the specification's mnemonic at index 0. If the pinned test ever fails, the derivation has changed and existing keys are no longer reproducible.

pkg/bip39wordlist

The BIP39 English wordlist, embedded from english.txt and checked against the reference implementation in the tests, plus mnemonic helpers:

  • ValidateWordlist(mnemonic) checks word count, every word, and the checksum, ignoring letter case and whitespace. It reports what is wrong with sentinel errors (ErrInvalidWordCount, ErrUnknownWord naming the word and its position, ErrInvalidChecksum).
  • NormalizeWordlist(mnemonic) returns the canonical lowercase, single-space-separated form, and refuses anything invalid.
  • GenerateMnemonic(wordCount) makes a new mnemonic of 12, 15, 18, 21, or 24 words from crypto/rand.
  • Contains(word), ValidWordCount(n), and the WordlistEnglish and Wordlist tables.

internal/randstream

An io.Reader that expands a 32-byte key with the ChaCha8 generator from math/rand/v2. With a seed string the key is its SHA-256 and the stream is deterministic; without one the key comes from crypto/rand, which is the only time the operating system's random source is touched. The output is for bulk data, not for cryptographic keys.

internal/pwgen

Produces passwords of the form

<word><digit><delim><word><digit><delim><word><digit>

where the delimiter is drawn once per password from .-_|~+=:,!@#%^&* and used in both positions. Words come from the system word list (/usr/share/dict/words) through github.com/kavfixnel/words, filtered to lowercase ASCII words of four to eight letters, which drops proper nouns and abbreviations. All choices use crypto/rand. On a system without a word list, sutil generate password panics rather than falling back to something weaker; the upstream library is only known to work on macOS.

CLI

sutil
├── version                 print the build version
├── info                    print version, Go version, module, VCS metadata
├── generate (gen)
│   ├── random [seedstr]    stream pseudorandom bytes (--count N to stop)
│   └── password (pw)       print a memorable password

sutil is not a command-line front end for the packages in pkg/. It holds only the handful of utilities the author wants available everywhere. pkg/cbk and pkg/bip39wordlist are libraries with no CLI surface, and a command is added because a tool is wanted, not because a package exists.

Build, test, and CI

The gates live in the Dockerfile, in the canonical Go layout that REPO_POLICIES.md prescribes: a lint phase on the hash-pinned golangci-lint image and a test phase on the Go image. script/lint and script/test each build one phase alone, with a fresh CACHEBUST value so the check step runs on every build instead of being served from Docker's cache, and write no image. The build stage depends on both phases, so the image cannot be built unless they pass. Go's build cache and golangci-lint's cache are mounted into the phases and survive between builds. Formatting is the one check that runs on the host: script/fmt-check runs gofmt and the prettier pinned in yarn.lock. script/check runs all three. script/cibuild, which the Gitea workflow calls, bootstraps, runs script/check, and then builds the tagged image with the version from git describe.

Every external reference, from base images to the GitHub Action in .gitea/workflows/check.yml, is pinned by content hash with a version and date comment. .golangci.yml is byte-identical to the canonical copy in the prompts repository and is never edited here; the lint phase pins the golangci-lint release that the policy names. cgo stays off: the Makefile exports CGO_ENABLED=0, binaries are static, and the test phase runs on the alpine Go image without -race, which is this repository's one departure from the canonical Dockerfile. The runtime image carries the binary and the source it was built from.

Version tags are created by the author alone, when the author decides. Tooling, contributors, and agents working in this repository do not propose, schedule, or create tags.

Entrypoints

This repository adheres to the Scripts to Rule Them All standard: normalized scripts in script/ are the entrypoints for the development workflow, and the Makefile targets are thin shims that call them. We provide:

  • script/bootstrap: install everything the repository needs, idempotently: make, git, go, and, because yarn.lock is present, node, yarn 1 and the pinned prettier.
  • script/setup: make a fresh clone ready for development by running script/bootstrap and then script/install-precommit.
  • script/projectname: print the project name, x, which script/docker and script/cibuild use as the image tag.
  • script/test: build the test phase of the Dockerfile with a new CACHEBUST value, writing no image.
  • script/lint: build the lint phase the same way.
  • script/fmt: gofmt and prettier, writing files.
  • script/fmt-check: the read-only twin of script/fmt.
  • script/check: script/test, script/lint, then script/fmt-check. It modifies nothing.
  • script/docker: build the image tagged x, passing the version from git describe.
  • script/cibuild: what CI runs: script/bootstrap, script/check, then the tagged image build.
  • script/precommit: run by the pre-commit hook. It refuses a commit whose go.mod or go.sum is not tidy, then runs script/check.
  • script/install-precommit: install that hook; make hooks shims to it.

The Makefile adds four targets of its own: make build, make install, make tidy, and make clean.

Development

make setup      # once: bootstrap and install the pre-commit hook
make check      # test, lint, fmt-check: what the hook and CI run
make fmt        # gofmt and prettier, writes files
make build      # build ./sutil on the host
make docker     # build the image tagged x

TODO

Nothing outstanding.

License

WTFPL. See LICENSE.

Author

@sneak <sneak@sneak.berlin>