From 047f347955fe2a5436c036bcf50c198d966fe280 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Mon, 5 Oct 2026 02:07:54 +0200 Subject: [PATCH] Format and check markdown with prettier in make fmt and fmt-check (closes #110) script/fmt and script/fmt-check follow the model scripts in the prompts repo: Go as before, plus prettier over every markdown file with 4-space tabs and proseWrap always. Prettier is pinned by hash in package.json and yarn.lock; script/bootstrap now installs node, yarn and prettier. The Dockerfile lint stage copies node and yarn from a node image pinned by hash and runs script/bootstrap, so its make fmt-check fails the build on unformatted markdown. Every markdown file is formatted once; wording is unchanged (CLAUDE.md's "*" list markers become "-"). Model: opus-5-5 --- CLAUDE.md | 144 ++++----- Dockerfile | 16 +- README.md | 13 +- TODO.md | 768 +++++++++++++++++++++----------------------- package.json | 5 + pkg/agehd/README.md | 73 +++-- pkg/bip85/README.md | 27 +- script/bootstrap | 7 +- script/fmt | 23 +- script/fmt-check | 20 ++ yarn.lock | 8 + 11 files changed, 589 insertions(+), 515 deletions(-) create mode 100644 package.json create mode 100644 yarn.lock diff --git a/CLAUDE.md b/CLAUDE.md index cee9fc3..9d3eda8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,95 +1,93 @@ # IMPORTANT RULES -* Claude is an inanimate tool. The spam that Claude attempts to insert into +- Claude is an inanimate tool. The spam that Claude attempts to insert into commit messages (which it erroneously refers to as "attribution") is not - attribution, as I am the sole author of code created using Claude. It is - corporate advertising for Anthropic and is therefore completely - unacceptable in commit messages. + attribution, as I am the sole author of code created using Claude. It is + corporate advertising for Anthropic and is therefore completely unacceptable + in commit messages. -* Tests should always be run before committing code. No commits should be - made that do not pass tests. +- Tests should always be run before committing code. No commits should be made + that do not pass tests. -* Code should always be formatted before committing. Do not commit - unformatted code. +- Code should always be formatted before committing. Do not commit unformatted + code. -* Code should always be linted and linter errors fixed before committing. - NEVER commit code that does not pass the linter. DO NOT modify the linter - config unless specifically instructed. +- Code should always be linted and linter errors fixed before committing. NEVER + commit code that does not pass the linter. DO NOT modify the linter config + unless specifically instructed. -* The test suite is fast and local. When running tests, NEVER run - individual parts of the test suite, always run the whole thing by running - "make test". +- The test suite is fast and local. When running tests, NEVER run individual + parts of the test suite, always run the whole thing by running "make test". -* Do not stop working on a task until you have reached the definition of - done provided to you in the initial instruction. Don't do part or most of - the work, do all of the work until the criteria for done are met. +- Do not stop working on a task until you have reached the definition of done + provided to you in the initial instruction. Don't do part or most of the work, + do all of the work until the criteria for done are met. -* When you complete each task, if the tests are passing and the code is - formatted and there are no linter errors, always commit and push your - work. Use a good commit message and don't mention any author or co-author +- When you complete each task, if the tests are passing and the code is + formatted and there are no linter errors, always commit and push your work. + Use a good commit message and don't mention any author or co-author attribution. -* Do not create additional files in the root directory of the project - without asking permission first. Configuration files, documentation, and - build files are acceptable in the root, but source code and other files - should be organized in appropriate subdirectories. +- Do not create additional files in the root directory of the project without + asking permission first. Configuration files, documentation, and build files + are acceptable in the root, but source code and other files should be + organized in appropriate subdirectories. -* Do not use bare strings or numbers in code, especially if they appear - anywhere more than once. Always define a constant (usually at the top of - the file) and give it a descriptive name, then use that constant in the - code instead of the bare string or number. +- Do not use bare strings or numbers in code, especially if they appear anywhere + more than once. Always define a constant (usually at the top of the file) and + give it a descriptive name, then use that constant in the code instead of the + bare string or number. -* If you are fixing a bug, write a test first that reproduces the bug and - fails, and then fix the bug in the code, using the test to verify that the - fix worked. +- If you are fixing a bug, write a test first that reproduces the bug and fails, + and then fix the bug in the code, using the test to verify that the fix + worked. -* When implementing new features, be aware of potential side-effects (such - as state files on disk, data in the database, etc.) and ensure that it is - possible to mock or stub these side-effects in tests when designing an - API. +- When implementing new features, be aware of potential side-effects (such as + state files on disk, data in the database, etc.) and ensure that it is + possible to mock or stub these side-effects in tests when designing an API. -* When dealing with dates and times or timestamps, always use, display, and - store UTC. Set the local timezone to UTC on startup. If the user needs - to see the time in a different timezone, store the user's timezone in a - separate field and convert the UTC time to the user's timezone when - displaying it. For internal use and internal applications and - administrative purposes, always display UTC. +- When dealing with dates and times or timestamps, always use, display, and + store UTC. Set the local timezone to UTC on startup. If the user needs to see + the time in a different timezone, store the user's timezone in a separate + field and convert the UTC time to the user's timezone when displaying it. For + internal use and internal applications and administrative purposes, always + display UTC. -* When implementing programs, put the main.go in - ./cmd//main.go and put the program's code in - ./internal//. This allows for multiple programs to be - implemented in the same repository without cluttering the root directory. - main.go should simply import and call .CLIEntry(). The - full implementation should be in ./internal//. +- When implementing programs, put the main.go in ./cmd//main.go + and put the program's code in ./internal//. This allows for + multiple programs to be implemented in the same repository without cluttering + the root directory. main.go should simply import and call + .CLIEntry(). The full implementation should be in + ./internal//. -* When you are instructed to make the tests pass, DO NOT delete tests, skip - tests, or change the tests specifically to make them pass (unless there - is a bug in the test). This is cheating, and it is bad. You should only - be modifying the test if it is incorrect or if the test is no longer - relevant. In almost all cases, you should be fixing the code that is - being tested, or updating the tests to match a refactored implementation. +- When you are instructed to make the tests pass, DO NOT delete tests, skip + tests, or change the tests specifically to make them pass (unless there is a + bug in the test). This is cheating, and it is bad. You should only be + modifying the test if it is incorrect or if the test is no longer relevant. In + almost all cases, you should be fixing the code that is being tested, or + updating the tests to match a refactored implementation. -* Always write a `Makefile` with the default target being `test`, and with a - `fmt` target that formats the code. The `test` target should run all - tests in the project, and the `fmt` target should format the code. `test` - should also have a prerequisite target `lint` that should run any linters - that are configured for the project. +- Always write a `Makefile` with the default target being `test`, and with a + `fmt` target that formats the code. The `test` target should run all tests in + the project, and the `fmt` target should format the code. `test` should also + have a prerequisite target `lint` that should run any linters that are + configured for the project. -* After each completed bugfix or feature, the code must be committed. Do - all of the pre-commit checks (test, lint, fmt) before committing, of - course. After each commit, push to the remote. +- After each completed bugfix or feature, the code must be committed. Do all of + the pre-commit checks (test, lint, fmt) before committing, of course. After + each commit, push to the remote. -* Always write tests, even if they are extremely simple and just check for - correct syntax (ability to compile/import). If you are writing a new - feature, write a test for it. You don't need to target complete coverage, - but you should at least test any new functionality you add. +- Always write tests, even if they are extremely simple and just check for + correct syntax (ability to compile/import). If you are writing a new feature, + write a test for it. You don't need to target complete coverage, but you + should at least test any new functionality you add. -* Always use structured logging. Log any relevant state/context with the - messages (but do not log secrets). If stdout is not a terminal, output - the structured logs in jsonl format. Use go's log/slog. +- Always use structured logging. Log any relevant state/context with the + messages (but do not log secrets). If stdout is not a terminal, output the + structured logs in jsonl format. Use go's log/slog. -* You do not need to summarize your changes in the chat after making them. - Making the changes and committing them is sufficient. If anything out of - the ordinary happened, please explain it, but in the normal case where you - found and fixed the bug, or implemented the feature, there is no need for - the end-of-change summary. +- You do not need to summarize your changes in the chat after making them. + Making the changes and committing them is sufficient. If anything out of the + ordinary happened, please explain it, but in the normal case where you found + and fixed the bug, or implemented the feature, there is no need for the + end-of-change summary. diff --git a/Dockerfile b/Dockerfile index de226b1..d0afa31 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,22 @@ +# node and yarn, copied into the lint stage for prettier, which checks the +# markdown formatting: node of the version script/bootstrap pins, built on +# Debian as the lint stage's image is. +# node:22.17.0-bookworm-slim, 2025-07-08 +FROM node@sha256:b04ce4ae4e95b522112c2e5c52f781471a5cbc3b594527bcddedee9bc48c03a0 AS node + # Lint stage — fast feedback on formatting and lint issues # golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-07 FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint +COPY --from=node /usr/local/bin/node /usr/local/bin/node +COPY --from=node /opt/yarn-v1.22.22 /opt/yarn-v1.22.22 +ENV PATH="/opt/yarn-v1.22.22/bin:${PATH}" + +# script/bootstrap downloads the Go modules and installs prettier WORKDIR /src -COPY go.mod go.sum ./ -RUN go mod download +COPY script/ script/ +COPY go.mod go.sum package.json yarn.lock ./ +RUN script/bootstrap # script/cibuild sets CHECK_EPOCH to the current time, so the RUN steps # below run again on each build, an unchanged tree included, while the diff --git a/README.md b/README.md index 46d6d88..29d38f7 100644 --- a/README.md +++ b/README.md @@ -593,8 +593,10 @@ 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 all dependencies (Go, Go module download), - idempotently; golangci-lint is not installed, it runs in docker +- `script/bootstrap` — install all dependencies (Go, Go module download, and + node, yarn and prettier for formatting markdown), idempotently; prettier is + pinned by hash in `package.json` and `yarn.lock`; golangci-lint is not + installed, it runs in docker - `script/setup` — make a fresh clone ready for development: runs `script/bootstrap`, then `script/install-precommit` - `script/projectname` — output the project name (`secret`); used by other @@ -611,8 +613,11 @@ provide: compiles; cgo is off, so the keychain unlocker's calls into the keychain (`internal/secret/keychainunlocker_cgo.go`, and `keychainunlocker_test.go`) and the Secure Enclave bindings (`internal/macse`) are not checked -- `script/fmt` — format all Go code (writes) -- `script/fmt-check` — check formatting without writing +- `script/fmt` — format all Go code with `go fmt` and every markdown file with + prettier (4-space tabs, `proseWrap: always`) (writes) +- `script/fmt-check` — check the same formatting without writing; the + `Dockerfile` lint stage runs it, so an unformatted Go or markdown file fails + the build - `script/check` — run `script/test`, `script/lint`, `script/lint-darwin`, and `script/fmt-check` - `script/docker` — build the Docker image tagged with the project name diff --git a/TODO.md b/TODO.md index 537ee82..1341a84 100644 --- a/TODO.md +++ b/TODO.md @@ -18,462 +18,442 @@ https://git.eeqj.de/sneak/secret/milestone/12 # Completed Steps +- 2026-10-04: `make fmt` formats every markdown file with prettier (4-space + tabs, `proseWrap: always`) as well as the Go code, and `make fmt-check` checks + both, as the model scripts in the `prompts` repo do + (https://git.eeqj.de/sneak/secret/issues/110). Prettier is pinned by hash in + `package.json` and `yarn.lock`, and `script/bootstrap` installs node, yarn and + prettier. The `Dockerfile` lint stage copies node and yarn from a node image + pinned by hash and runs `script/bootstrap`, so its `make fmt-check` fails the + build on an unformatted markdown file. Every markdown file was formatted once, + wording unchanged. - 2026-10-04: A mnemonic that cannot be read, in `secret init` and `secret vault create`, gives an error that names the mnemonic only (https://git.eeqj.de/sneak/secret/issues/115). It is read with `secret.ReadMnemonic`, whose every error wraps the new - `secret.ErrMnemonicNotRead`; before, it was read with `ReadPassphrase`, so - the message said "failed to read mnemonic: failed to read passphrase:" and - advised setting `SB_UNLOCK_PASSPHRASE`. Without a terminal it now says - "failed to read mnemonic: stdin is not a terminal (piped input or script). - Please set the SB_SECRET_MNEMONIC environment variable or run - interactively". The passphrase messages no longer repeat "cannot read - passphrase" after "failed to read passphrase:", and empty input gives - "nothing was entered". -- 2026-10-04: A failure returns the same error value whichever command hits - it (https://git.eeqj.de/sneak/secret/issues/113). `internal/cli` no longer - keeps its own copies of `vault.ErrSecretNotFound`, `ErrVaultNotFound`, + `secret.ErrMnemonicNotRead`; before, it was read with `ReadPassphrase`, so the + message said "failed to read mnemonic: failed to read passphrase:" and advised + setting `SB_UNLOCK_PASSPHRASE`. Without a terminal it now says "failed to read + mnemonic: stdin is not a terminal (piped input or script). Please set the + SB_SECRET_MNEMONIC environment variable or run interactively". The passphrase + messages no longer repeat "cannot read passphrase" after "failed to read + passphrase:", and empty input gives "nothing was entered". +- 2026-10-04: A failure returns the same error value whichever command hits it + (https://git.eeqj.de/sneak/secret/issues/113). `internal/cli` no longer keeps + its own copies of `vault.ErrSecretNotFound`, `ErrVaultNotFound`, `ErrVersionNotFound` and `ErrSecretExists`: `secret mv`, `rm`, `decrypt`, - `vault import`, `vault remove` and `version list`, `promote` and `rm` wrap - the `vault` errors. `errUnsupportedUnlockerType` is removed: `secret - unlocker add` gives `errInvalidUnlockerType` for an unknown type, whichever - check rejects it. Off macOS, adding a keychain or Secure Enclave unlocker - returns the `secret` package's error for it, not an `internal/cli` copy; on - macOS, the check that the system is macOS is gone, as it could never fail. + `vault import`, `vault remove` and `version list`, `promote` and `rm` wrap the + `vault` errors. `errUnsupportedUnlockerType` is removed: `secret unlocker add` + gives `errInvalidUnlockerType` for an unknown type, whichever check rejects + it. Off macOS, adding a keychain or Secure Enclave unlocker returns the + `secret` package's error for it, not an `internal/cli` copy; on macOS, the + check that the system is macOS is gone, as it could never fail. `secret vault import` gives `errInvalidMnemonicPhrase` for an invalid mnemonic, as `init` and `vault create` do. `secret generate secret` gives `errLengthTooSmall` for a length below 1 wherever it is checked, and `errUnsupportedSecretType` for `--type mnemonic` too. `secret import` of a file over 100MB wraps `errSecretTooLarge`, as `secret add` returns it. `vault.ErrNilValueBuffer` is replaced by `secret.ErrNilValueBuffer`, which - `secret` already returned under another name. Messages are unchanged, - except that `secret decrypt` of a missing secret says "not found", as - `secret get` does, not "does not exist"; `vault import` of an invalid - mnemonic says "invalid BIP39 mnemonic phrase"; `--type mnemonic` says - "unsupported type: mnemonic (use 'secret generate mnemonic' instead)"; and - a file too large to import says + `secret` already returned under another name. Messages are unchanged, except + that `secret decrypt` of a missing secret says "not found", as `secret get` + does, not "does not exist"; `vault import` of an invalid mnemonic says + "invalid BIP39 mnemonic phrase"; `--type mnemonic` says "unsupported type: + mnemonic (use 'secret generate mnemonic' instead)"; and a file too large to + import says `failed to read secret from file : secret too large: exceeds 100MB limit`. Every error of `secret.ReadPassphrase` wraps `secret.ErrPassphraseNotRead`, - which supplies the words "failed to read passphrase" that its callers used - to add themselves; so two passphrases that differ now give only - "passphrases do not match", the words now follow "failed to read mnemonic:" - and "failed to read passphrase confirmation:", and a terminal read error no - longer repeats them. A GPG key the keyring does not hold gives - `secret.ErrGPGKeyNotFound`, found by gpg's status line for "No public key"; - before, the message repeated "failed to resolve GPG key fingerprint" and - ended in gpg's exit status. The keychain unlocker returns `errNilDataBuffer` - for nil data; this and its test build only on macOS with cgo and were only - read. `bip85.ErrPasswordTooShort` and `ErrEncodedTooShort` are removed with - their checks: 64 bytes of entropy always give 86 Base64 or 80 Base85 - characters, the most a password length may ask for. Tests that matched - these errors' text use `errors.Is`. -- 2026-10-04: Tests check which error a failure returns with `errors.Is`, - not by matching words of its message - (https://git.eeqj.de/sneak/secret/issues/49). Every exported error that - can be returned has a test that the function returns it, and errors - wrapping a cause are checked through the wrapping. Checks that still match - text, because the error has no exported value the test can name, are - listed on the issue. + which supplies the words "failed to read passphrase" that its callers used to + add themselves; so two passphrases that differ now give only "passphrases do + not match", the words now follow "failed to read mnemonic:" and "failed to + read passphrase confirmation:", and a terminal read error no longer repeats + them. A GPG key the keyring does not hold gives `secret.ErrGPGKeyNotFound`, + found by gpg's status line for "No public key"; before, the message repeated + "failed to resolve GPG key fingerprint" and ended in gpg's exit status. The + keychain unlocker returns `errNilDataBuffer` for nil data; this and its test + build only on macOS with cgo and were only read. `bip85.ErrPasswordTooShort` + and `ErrEncodedTooShort` are removed with their checks: 64 bytes of entropy + always give 86 Base64 or 80 Base85 characters, the most a password length may + ask for. Tests that matched these errors' text use `errors.Is`. +- 2026-10-04: Tests check which error a failure returns with `errors.Is`, not by + matching words of its message (https://git.eeqj.de/sneak/secret/issues/49). + Every exported error that can be returned has a test that the function returns + it, and errors wrapping a cause are checked through the wrapping. Checks that + still match text, because the error has no exported value the test can name, + are listed on the issue. - 2026-10-04: When a vault cannot be opened through its current unlocker, - because a file the unlocker needs is missing or damaged, its keychain item - or Secure Enclave key is gone, or the passphrase is wrong, the error now - ends by naming the vault, saying that it still opens with its mnemonic, - and that `secret unlocker add passphrase`, run with `SB_SECRET_MNEMONIC` - set to it, gives the vault a new unlocker; for a vault that is not the - current one, as in `secret move` between vaults, it says to run - `secret vault select` first (https://git.eeqj.de/sneak/secret/issues/47). - Before, it ended with the bare cause. The advice is given only when the - vault metadata records the key the mnemonic derives, so not for a vault - created without a mnemonic, and not when the passphrase could not be read - at all. `secret vault import` is not named: it refuses a vault that has a - long-term key. `secret encrypt` and `secret decrypt` now read the key - secret through `vault.GetSecret`, as `secret get` does, so they give the - same advice; `Secret.GetValue`, the other way to get the long-term key, is - removed. When a secret's `current` file cannot be read, the error says - that `secret version list` lists its versions and `secret version promote` - makes one current. The causes stay wrapped. -- 2026-10-04: An unlocker's ID is the name of its directory in `unlockers.d`, - so no two unlockers of a vault share one + because a file the unlocker needs is missing or damaged, its keychain item or + Secure Enclave key is gone, or the passphrase is wrong, the error now ends by + naming the vault, saying that it still opens with its mnemonic, and that + `secret unlocker add passphrase`, run with `SB_SECRET_MNEMONIC` set to it, + gives the vault a new unlocker; for a vault that is not the current one, as in + `secret move` between vaults, it says to run `secret vault select` first + (https://git.eeqj.de/sneak/secret/issues/47). Before, it ended with the bare + cause. The advice is given only when the vault metadata records the key the + mnemonic derives, so not for a vault created without a mnemonic, and not when + the passphrase could not be read at all. `secret vault import` is not named: + it refuses a vault that has a long-term key. `secret encrypt` and + `secret decrypt` now read the key secret through `vault.GetSecret`, as + `secret get` does, so they give the same advice; `Secret.GetValue`, the other + way to get the long-term key, is removed. When a secret's `current` file + cannot be read, the error says that `secret version list` lists its versions + and `secret version promote` makes one current. The causes stay wrapped. +- 2026-10-04: An unlocker's ID is the name of its directory in `unlockers.d`, so + no two unlockers of a vault share one (https://git.eeqj.de/sneak/secret/issues/98). Before, a keychain or Secure Enclave unlocker's ID was its creation time to the minute and the host name, and a passphrase unlocker's the time to the minute, so two created within a minute shared an ID, and `unlocker select`, `unlocker remove` and the - selection `unlocker add` makes acted on the older one. A PGP unlocker's ID - was `pgp-` and its key's fingerprint; a second PGP unlocker for a key is - still refused, now by comparing the fingerprint in the other unlockers' - metadata. `unlocker list` and the shell completion of `unlocker select` and - `unlocker remove` take each ID from the directory the unlocker was read - from, no longer by matching metadata, so two unlockers with the same - metadata are listed apart; an unlocker of an unknown type is listed under - its directory name, and completion now offers Secure Enclave unlockers too. - The keychain and Secure Enclave code was type-checked by - `script/lint-darwin`, never run; a test on Linux lists, completes, selects - and removes each of two passphrase unlockers with the same metadata by its - own ID. -- 2026-10-04: README's Storage Architecture, `secret version promote`, - Technical Details and Testing text matches the code - (https://git.eeqj.de/sneak/secret/issues/102). `current` and - `currentvault` are plain files holding a name, not symbolic links; a - version's metadata is the encrypted `metadata.age`; the state directory is - `berlin.sneak.pkg.secret` in the user's configuration directory, not - `~/.local/share/secret`, and holds the `lock` file. Also corrected: the - code sets up no Touch ID for the keychain or Secure Enclave unlocker, and - the Secure Enclave only decrypts; per-version keys give no forward - secrecy; `pub.age` is not age-encrypted; vault metadata holds no vault - name. Testing lists only `make test`. + selection `unlocker add` makes acted on the older one. A PGP unlocker's ID was + `pgp-` and its key's fingerprint; a second PGP unlocker for a key is still + refused, now by comparing the fingerprint in the other unlockers' metadata. + `unlocker list` and the shell completion of `unlocker select` and + `unlocker remove` take each ID from the directory the unlocker was read from, + no longer by matching metadata, so two unlockers with the same metadata are + listed apart; an unlocker of an unknown type is listed under its directory + name, and completion now offers Secure Enclave unlockers too. The keychain and + Secure Enclave code was type-checked by `script/lint-darwin`, never run; a + test on Linux lists, completes, selects and removes each of two passphrase + unlockers with the same metadata by its own ID. +- 2026-10-04: README's Storage Architecture, `secret version promote`, Technical + Details and Testing text matches the code + (https://git.eeqj.de/sneak/secret/issues/102). `current` and `currentvault` + are plain files holding a name, not symbolic links; a version's metadata is + the encrypted `metadata.age`; the state directory is `berlin.sneak.pkg.secret` + in the user's configuration directory, not `~/.local/share/secret`, and holds + the `lock` file. Also corrected: the code sets up no Touch ID for the keychain + or Secure Enclave unlocker, and the Secure Enclave only decrypts; per-version + keys give no forward secrecy; `pub.age` is not age-encrypted; vault metadata + holds no vault name. Testing lists only `make test`. - 2026-10-04: `secret init` and `secret vault create` create a vault whole or - not at all (https://git.eeqj.de/sneak/secret/issues/105). - `vault.CreateVault` now takes the unlocker passphrase too, writes the vault - directory with its metadata, long-term public key and passphrase unlocker, - `longterm.age` included, into a temporary directory, renames that into - `vaults.d` once it is complete, and only then makes the vault current. - Before, either command killed after the passphrase prompt but before the - unlocker was written left a vault with no unlocker, which `vault create` had - already made current and which neither command would create again. Killed - part-way now, it leaves no vault, and the next command that takes the lock - deletes the temporary directory; or, killed between the rename and making - the vault current, a complete vault that is not current, which - `secret vault select` makes current. + not at all (https://git.eeqj.de/sneak/secret/issues/105). `vault.CreateVault` + now takes the unlocker passphrase too, writes the vault directory with its + metadata, long-term public key and passphrase unlocker, `longterm.age` + included, into a temporary directory, renames that into `vaults.d` once it is + complete, and only then makes the vault current. Before, either command killed + after the passphrase prompt but before the unlocker was written left a vault + with no unlocker, which `vault create` had already made current and which + neither command would create again. Killed part-way now, it leaves no vault, + and the next command that takes the lock deletes the temporary directory; or, + killed between the rename and making the vault current, a complete vault that + is not current, which `secret vault select` makes current. - 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. - `macse.CreateKey` finds the new key's hash right after `sc_auth` creates - it, and fails with an error naming the key's label if it cannot; it deletes - the key again if getting its public key then fails. The Objective-C was only - read, never compiled or run, and so was `macse_darwin.go`, which is cgo only. + `macse.CreateKey` finds the new key's hash right after `sc_auth` creates it, + and fails with an error naming the key's label if it cannot; it deletes the + key again if getting its public key then fails. The Objective-C was only read, + never compiled or run, and so was `macse_darwin.go`, which is cgo only. `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. + 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: What a command killed part-way left under a `.tmp-` name - (https://git.eeqj.de/sneak/secret/issues/75), the temporary directories - of `secret.TempDirFor` and the temporary files of - `secret.WriteFileAtomic`, encrypted keys included, is deleted by the next - command that takes the state directory lock. Before, it stayed until - deleted by hand. A command writes `finished` into the lock file just - before it releases the lock; the next one to take the lock searches only - when it does not find that, so after a command that finished nothing is - searched, however many secrets and versions there are. The search looks - in the state directory, each vault, each secret and each version, the - only directories those helpers make them in. A command that only reads - takes no lock and deletes nothing. A failure to delete is warned about + (https://git.eeqj.de/sneak/secret/issues/75), the temporary directories of + `secret.TempDirFor` and the temporary files of `secret.WriteFileAtomic`, + encrypted keys included, is deleted by the next command that takes the state + directory lock. Before, it stayed until deleted by hand. A command writes + `finished` into the lock file just before it releases the lock; the next one + to take the lock searches only when it does not find that, so after a command + that finished nothing is searched, however many secrets and versions there + are. The search looks in the state directory, each vault, each secret and each + version, the only directories those helpers make them in. A command that only + reads takes no lock and deletes nothing. A failure to delete is warned about and the command goes on. An unlocker directory with no metadata file was - already removed by `secret unlocker remove` given its directory name; a - test now shows it. -- 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`. + already removed by `secret unlocker remove` given its directory name; a test + now shows it. +- 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 + (`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. + `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. + 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-