# Workflow * branch (from `main`) * do the work in Next Step * move Next Step to the top of Completed Steps * move the top item of Future Steps into Next Step * commit (`TODO.md` changes in the same commit as the work) * merge to `main` if the branch is not protected, otherwise open a PR * push # Status pre-1.0. No git tags. TODO.md carries open 1.0 security blockers. Work in flight on branch secure-enclave-unlocker (clean tree as of 2026-07-06). # Next Step Bring the repo into policy compliance in one commit: - Add fmt-check and hooks targets to the Makefile (test/lint/fmt/check/ docker already exist). - Add REPO_POLICIES.md and .editorconfig. - Add .gitea/workflows/check.yml running make check. - Verify Dockerfile base images are pinned by sha256. # Completed Steps - 2026-10-04: A failed `secret unlocker add keychain` or `secret unlocker add secure-enclave` no longer leaves its keychain item or Secure Enclave key behind (https://git.eeqj.de/sneak/secret/issues/89). `CreateSecureEnclaveUnlocker` gets the long-term key before it creates the Secure Enclave key, so that a wrong passphrase creates none, and deletes the key again if encrypting with it or writing the unlocker then fails. `CreateKeychainUnlocker` writes all of the unlocker's files, the metadata among them, before it stores the item in the keychain, and deletes the item again if moving the unlocker into place then fails. A failure to delete is reported along with the first error. The tests of this run only on macOS: the Secure Enclave one in a build with cgo on a Mac with a Secure Enclave, the keychain one in a build with cgo. - 2026-10-04: An age identity's private key goes into a locked buffer through `secret.IdentityToLockedBuffer` everywhere (https://git.eeqj.de/sneak/secret/issues/38): the vault's long-term key when a passphrase, PGP, keychain or Secure Enclave unlocker is created, the new unlocker's own key, a new secret version's key, and the key `secret encrypt` generates. Before, each place converted the string age returns to bytes and left the string in ordinary memory. The function moves the string's own bytes into the buffer, which overwrites them; the copies age makes while writing the string remain, as its comment says. The 1.0 memory-security entry below no longer lists these places, `internal/cli/crypto.go` among them, nor `version.go:155`, which was `internal/secret/version.go`, not `internal/cli/version.go`. - 2026-10-04: `script/lint-darwin` (`make lint-darwin`) runs `go vet` and `golangci-lint` in docker on the code as a macOS build compiles it (`GOOS=darwin`), with cgo off (https://git.eeqj.de/sneak/secret/issues/50). `script/check` runs it, and the `Dockerfile` lint stage runs its commands, so `script/cibuild` does too. Before, CI on Linux never compiled the files built only for macOS. Compiling cgo code for macOS needs Apple's SDK headers, and both `internal/macse` and `github.com/keybase/go-keychain` are cgo on macOS. So the three functions that call `go-keychain` moved from `keychainunlocker.go` to `keychainunlocker_cgo.go`, built only with cgo on macOS like `macse_darwin.go`. A macOS build without cgo, which before did not compile, gets `keychainunlocker_nocgo.go` and the `macse` stub instead, whose errors say the keychain or Secure Enclave needs a macOS build with cgo. The check covers the rest of the keychain unlocker, the Secure Enclave unlocker and the macOS-only tests other than `keychainunlocker_test.go`, whose lint findings are fixed. For the length and complexity limits, parts of `GetIdentity`, `getLongTermPrivateKey` and `CreateKeychainUnlocker` moved into functions of their own, and the Secure Enclave unlocker derives the long-term key from the mnemonic through the same function as the keychain unlocker instead of a copy of it. Lines over 88 columns in the files the check cannot see are wrapped. - 2026-10-04: `secret rm`, `secret version rm`, `secret vault remove` and `secret unlocker remove` ask `[y/N]` before removing anything (https://git.eeqj.de/sneak/secret/issues/39), naming what they remove: the secret, its vault and its version count; the version, secret and vault; the vault and its secret count; the unlocker, its vault and whether it is the last, and for the last the vault's secret count and that the vault then opens only with its mnemonic. Only `y` or `yes` goes ahead. Without `--force`, a command whose stdin is not a terminal fails at once. `--force` (now also on `rm` and `version rm`) removes without asking; it replaces the old refusals to remove a vault with secrets or the last unlocker of one without `--force`, which the question now covers. The checks run, and the question is asked, before the state directory lock is taken; under the lock the checks run again, and if they would ask a different question, nothing is removed. `secret rm` fails when it cannot count the versions. - 2026-10-04: A crash while an unlocker is being replaced no longer leaves a current unlocker that cannot open the vault (https://git.eeqj.de/sneak/secret/issues/71). Every new unlocker gets a directory of its own, named with the time to the nanosecond: `passphrase-