ssh install works over sftp, runs nothing on the host, and writes nothing when ~/.ssh or authorized_keys cannot be read or entered, or authorized_keys is a symlink (the defect of #12).
The mnemonic never reaches the ssh and sftp children, and extra spaces or line breaks in it cannot change a key.
A signal ends every command; the ssh commands remove their working files first, and an interrupted age -o leaves no unfinished file. -o keeps a symlink, pipe, device or redirected output stream at its path.
The README's SSH, age and child-mnemonic vectors are checked by the tests.
Module sneak.berlin/go/keyfunc, entrypoint cmd/keyfunc; --version shows the module version after go install, the tag or short commit from docker build ..
MIT license, as you chose.
The mnemonic library, whose repository is gone, is copied into internal/bip39, as you ruled; no derived key changes. Its two go.sum lines stay while a test in sneak/secret's bip85 imports it (sneak/secret#122).
Build and tooling follow the sneak/prompts templates at commit dd4027b; the image's last stage is a development environment, and script/cibuild needs only Docker and git.
To know before merging:
ssh install uses sftp batch mode: a key or an agent must authenticate, and the host key must already be known. Options after -- go to sftp.
go install sneak.berlin/go/keyfunc/cmd/keyfunc@latest resolves once main carries the module move.
btcd stays at v0.25.0: later releases drop packages that sneak/secret's bip85 imports. Every other direct dependency is current.
Model: opus-5-5
What is on `next`, all reviewed.
- `ssh install` works over `sftp`, runs nothing on the host, and writes nothing when `~/.ssh` or `authorized_keys` cannot be read or entered, or `authorized_keys` is a symlink (the defect of https://git.eeqj.de/sneak/keyfunc/pulls/12).
- The mnemonic never reaches the `ssh` and `sftp` children, and extra spaces or line breaks in it cannot change a key.
- A signal ends every command; the `ssh` commands remove their working files first, and an interrupted `age -o` leaves no unfinished file. `-o` keeps a symlink, pipe, device or redirected output stream at its path.
- The README's SSH, age and child-mnemonic vectors are checked by the tests.
- Module `sneak.berlin/go/keyfunc`, entrypoint `cmd/keyfunc`; `--version` shows the module version after `go install`, the tag or short commit from `docker build .`.
- MIT license, as you chose.
- The mnemonic library, whose repository is gone, is copied into `internal/bip39`, as you ruled; no derived key changes. Its two `go.sum` lines stay while a test in `sneak/secret`'s `bip85` imports it (https://git.eeqj.de/sneak/secret/issues/122).
- Build and tooling follow the `sneak/prompts` templates at commit `dd4027b`; the image's last stage is a development environment, and `script/cibuild` needs only Docker and git.
To know before merging:
- `ssh install` uses `sftp` batch mode: a key or an agent must authenticate, and the host key must already be known. Options after `--` go to `sftp`.
- `go install sneak.berlin/go/keyfunc/cmd/keyfunc@latest` resolves once `main` carries the module move.
- `btcd` stays at v0.25.0: later releases drop packages that `sneak/secret`'s `bip85` imports. Every other direct dependency is current.
Model: opus-5-5
ssh install no longer runs a command on the host. It reads .ssh/authorized_keys over sftp, takes the empty reading only from sftp's own message about that path, appends the derived key locally when it is not already present, uploads the result beside the file with mode 0600 and renames it over the original. Any other failure prints what sftp said, writes nothing and exits 1. sftp batch mode disables password prompts, so a key or agent is required; a directory the owner cannot enter reads as a host with no file, which README.md states.
Model: opus-5 (implementation); fable-5-1 (landing)
main.go moves unchanged to cmd/keyfunc/main.go, where REPO_POLICIES.md puts Go entrypoints, and the Makefile build target builds ./cmd/keyfunc. The binary is still written to ./keyfunc and still carries the stamped version.
Model: opus-4-8 (implementation); fable-5-1 (summary)
Every direct dependency moves to its current release, golang.org/x/crypto first: keyfunc ssh to serves keys through its ssh/agent package, which has had security fixes since the pinned 2025-05 version. go.mod and go.sum only; no code changed and the SSH, age and child mnemonic test vectors pass unedited, so no derived key moves.
Model: opus-4-8 (implementation); fable-5-1 (summary)
keyfunc --version printed dev for any binary not built with make build. When no version was stamped at build time, the tool now reports the module version recorded in the binary's build info, which go install fills in. A stamped version still wins, and a local build with neither still prints dev.
Model: opus-4-8 (implementation); fable-5-1 (summary)
The first sftp session now lists .ssh before fetching authorized_keys. The file reads as empty only when sftp reports .ssh itself as missing, or the listing succeeded and the file is reported missing. A directory or file that is there but cannot be read fails the run and nothing is written, so no existing authorized_keys is replaced by content that was not built from what was read. An .ssh that already exists keeps its mode; the directory is made and set to 0700 only when none was found. The README describes the rule and states batch mode's limit: a key or an agent must authenticate.
Model: opus-4-8 (implementation); fable-5-1 (summary)
`keyfunc ssh to` and `keyfunc ssh install` started the system `ssh` and `sftp` with the tool's whole environment, so a mnemonic given in `KEYFUNC_MNEMONIC` stayed readable in the child's environment and could be forwarded to the host by a `SendEnv` line. Both children now get the environment with `KEYFUNC_MNEMONIC` and `KEYFUNC_MNEMONIC_COMMAND` removed, through one helper, `childEnv`, in the ssh cli package. The mnemonic command still runs with the full environment. Two tests drive the real commands against the stand-in `ssh` and `sftp` and check that a third variable still arrives.
Model: opus-4-8 (implementation, review); fable-5-1 (merge message)
The README gains the sections REPO_POLICIES.md requires: a first sentence naming the category and author, Getting Started, Entrypoints (one line per `script/` file), Rationale, Design, TODO (the open issues between the tree and 1.0), License and Author. It also publishes test vectors for age and child mnemonics, copied from the tests.
Disclosures:
- No license is named; the choice is open on the tracker and the README says so.
- The 12-word child mnemonic is the BIP-85 specification vector, the only one the test asserts, and is labelled as such.
- Markdown is hand-wrapped; `make fmt` here formats Go only.
Model: opus-4-8 (implementation, review); fable-5-1 (merge message)
`cli.Main` ran the command tree on a background context, so SIGINT, SIGTERM or SIGHUP killed the process before deferred cleanup ran: `ssh to` left its agent socket and directory behind, and `ssh install` left a copy of the host's `authorized_keys` in its working directory. `Main` now runs the tree on a `signal.NotifyContext` for those signals; the cancelled context ends the child `ssh` or `sftp` and the cleanup runs. `ssh to` stops its child with SIGTERM, not a kill, so `ssh` restores the terminal. Exit status after a signal is 1 unless `ssh` reported its own.
The test re-runs the test binary as the tool, waits for the agent socket, sends each signal and checks the directory is gone.
Disclosure: the repeated `"uptime"` test literal became a `remoteCommand` constant because `goconst` required it.
Model: opus-4-8 (implementation, review); fable-5-1 (merge message)
golangci-lint 2.12 deprecated `gomodguard` in favour of `gomodguard_v2` and printed a warning on every `make check`. `.golangci.yml` now disables the old name, the same way it already handles `wsl` and `wsl_v5`. With `linters.default: all` the replacement was already enabled, so what is checked does not change; only the warning goes.
Model: opus-4-8 (implementation, review); fable-5-1 (merge message)
Every example in the README was run as written with the published test mnemonic and behaved as the README says, so no sentence changed. The only edit removes the landed work from the TODO section, which now lists the two open owner decisions.
Disclosures:
- `ssh install` and `ssh to` were run by the implementer against a throwaway local `sshd`; the reviewer could not repeat that run and checked those sections by reading the code.
- The child mnemonic vector is reachable only through the test suite and was confirmed there.
Model: opus-4-8 (implementation, review); fable-5-1 (merge message)
.dockerignore left out .git, so make build inside the image fell back to
"dev". The build context now carries .git, without its config, which can
hold a credential in the remote URL. The build stage takes the VERSION
build argument when one is given, otherwise git describe --tags --always,
and fails if the context carries .git and no version comes out.
Model: opus-5-5
Adds LICENSE with the standard MIT text and "Copyright (c) 2026 sneak", the license sneak chose. The README now calls keyfunc MIT-licensed in its first paragraph, its License section points at LICENSE, and the license line leaves the TODO list.
Model: opus-5-5
The module path becomes sneak.berlin/go/keyfunc, as the repo policy sets for Go modules: go.mod, every import and the -X path in the Makefile. The old path gets no alias. The README gives a go install line for the new path, which resolves with @latest only once main carries the move, and its TODO list now names the open 1.0 issues.
Model: opus-5-5
.golangci.yml is now a byte-identical copy of the current template: depguard is on with the test-support rule, and the gomodguard_v2 block list is in. .gitignore and .dockerignore are the current templates with this repo's own entries added at the end; the .dockerignore secret-file patterns now keep key files and .env files out of the build context, while .git stays in for the version stamp. No Go source needed changes.
Model: opus-5-5
Lint and test are now phases of the one Dockerfile, as the current repo policy requires: a lint phase on the pinned golangci-lint image and a test phase on the pinned Go image, and the build stage depends on both, so a plain docker build . fails when either fails. Dockerfile.lint is gone. REPO_POLICIES.md and script/lint, test, docker and cibuild are byte-identical to the current sneak/prompts copies, so every docker build in script/ is uncached and tagged. make test now needs Docker on the host; formatting is checked on the host only.
Judgement calls: the test phase installs gcc and musl-dev unpinned for -race; no -count=1, since a build stage holds no earlier result.
Model: opus-5-5
script/cibuild runs script/bootstrap on the Gitea runner, which has Docker and git but no Go, and bootstrap could not install it: its apt path never ran apt-get update. Bootstrap now installs Go at the version in the Dockerfile's golang image from go.dev, checked against sha256 values in the script, into ~/.local/go whenever the go first on PATH reports another version; the Makefile and the fmt, fmt-check and precommit scripts put ~/.local/go/bin first on their PATH. apt-get update runs once before the first apt install. Bumping the golang digest now means bumping GO_VERSION and its four hashes.
Partially verified: checked in a clean ubuntu:24.04 container, not yet on the Gitea runner.
Model: opus-5-5
make fmt and make fmt-check now cover the Markdown files with prettier as well as the Go source: prettier 3.8.1 pinned in package.json and yarn.lock, four-space indents and proseWrap always in .prettierrc, all three copied unchanged from the sneak/prompts templates. script/bootstrap adds pinned node (through nvm from a hash-checked archive when none is installed), yarn and prettier after the pinned Go. README.md is reformatted by make fmt and its Entrypoints section says what each step needs.
Model: opus-5-5
Signals now end only ssh to and ssh install (internal/cli/cli.go, Main). SIGINT, SIGTERM and SIGHUP are caught for every command, but only the ssh and sftp children act on them. Ctrl-C at the mnemonic prompt does nothing until Enter is pressed, and age encrypt and age decrypt keep reading after a signal: an interrupted producer | keyfunc age encrypt -o file puts a complete, decryptable encryption of the cut-off input in place and exits 0. Acceptable: every command ends promptly with a non-zero status on those signals (catch them only while ssh or sftp runs, or have the prompt and the age copy stop on the cancelled context), an interrupted -o run puts no file in place, and a test covers the age case.
Whitespace inside the mnemonic silently changes every key (internal/mnemonic/mnemonic.go, checked). Only the ends are trimmed. The same twelve words separated by two spaces, a tab, or one per line (as a --mnemonic-command may print them) pass the checksum, but the seed is computed over the string as given, so the keys differ from the README vectors with no warning. Acceptable: derive from the words joined by single spaces (or refuse any other spacing), with a test that one-word-per-line and double-spaced input give the README vectors.
A build whose context is sent as a tar stream fails (Dockerfile, build stage). With docker build - < context.tar or the Docker Engine API, the files keep the sender's owner, git refuses /src as dubious ownership, the version comes out empty and the version check stops the build. Acceptable: RUN git config --system --add safe.directory /src in the build stage, as the canonical Dockerfile on the next branch of sneak/prompts now has.
The README's child mnemonic vector cannot be checked with keyfunc (README.md, Derived mnemonics; internal/cli/cli_test.go). The only vector starts from the BIP-85 specification's master key, which the tool cannot take, and no test pins what keyfunc mnemonic prints for a given mnemonic. Acceptable: the abandon … about child mnemonic at index 0 next to the SSH and age vectors, asserted by a test that runs the command.
ssh install also needs the host key to be known already (README.md, keyfunc ssh install; the PR body's batch-mode note). sftp batch mode refuses a host key that is not yet in known_hosts, so a first run against a new host fails with Host key verification failed; the README names only the key-or-agent limit. Acceptable: say so, with the way round it (connect once with ssh, or -o StrictHostKeyChecking=accept-new after --).
The CI checkout fetches no tags (.gitea/workflows/check.yml). REPO_POLICIES.md requires fetch-depth: 0 on the checkout step of a repo that stamps a version from git describe --tags; this one does not set it. Acceptable: with: fetch-depth: 0 on that step.
Two claims in the PR body are not true (#28). "The linter config and ignore files are the current templates": the next branch of sneak/prompts changed them and REPO_POLICIES.md on 2026-10-04 (golangci-lint v2.14.0 with exhaustruct_v5, .git/modules/**/config, the fix in item 3). "Every direct dependency is current": github.com/btcsuite/btcd stays at v0.25.0 while v0.26.2 is out, which moves chaincfg into a separate module. Acceptable: re-vendor those files along with item 3, or name the template date the branch carries; say why btcd is held.
Model: opus-5-5
1. **Signals now end only `ssh to` and `ssh install`** (`internal/cli/cli.go`, `Main`). SIGINT, SIGTERM and SIGHUP are caught for every command, but only the `ssh` and `sftp` children act on them. Ctrl-C at the mnemonic prompt does nothing until Enter is pressed, and `age encrypt` and `age decrypt` keep reading after a signal: an interrupted `producer | keyfunc age encrypt -o file` puts a complete, decryptable encryption of the cut-off input in place and exits 0. Acceptable: every command ends promptly with a non-zero status on those signals (catch them only while `ssh` or `sftp` runs, or have the prompt and the age copy stop on the cancelled context), an interrupted `-o` run puts no file in place, and a test covers the age case.
2. **Whitespace inside the mnemonic silently changes every key** (`internal/mnemonic/mnemonic.go`, `checked`). Only the ends are trimmed. The same twelve words separated by two spaces, a tab, or one per line (as a `--mnemonic-command` may print them) pass the checksum, but the seed is computed over the string as given, so the keys differ from the README vectors with no warning. Acceptable: derive from the words joined by single spaces (or refuse any other spacing), with a test that one-word-per-line and double-spaced input give the README vectors.
3. **A build whose context is sent as a tar stream fails** (`Dockerfile`, build stage). With `docker build - < context.tar` or the Docker Engine API, the files keep the sender's owner, git refuses `/src` as dubious ownership, the version comes out empty and the version check stops the build. Acceptable: `RUN git config --system --add safe.directory /src` in the build stage, as the canonical Dockerfile on the `next` branch of `sneak/prompts` now has.
4. **The README's child mnemonic vector cannot be checked with keyfunc** (`README.md`, Derived mnemonics; `internal/cli/cli_test.go`). The only vector starts from the BIP-85 specification's master key, which the tool cannot take, and no test pins what `keyfunc mnemonic` prints for a given mnemonic. Acceptable: the `abandon … about` child mnemonic at index 0 next to the SSH and age vectors, asserted by a test that runs the command.
5. **`ssh install` also needs the host key to be known already** (`README.md`, `keyfunc ssh install`; the PR body's batch-mode note). `sftp` batch mode refuses a host key that is not yet in `known_hosts`, so a first run against a new host fails with `Host key verification failed`; the README names only the key-or-agent limit. Acceptable: say so, with the way round it (connect once with `ssh`, or `-o StrictHostKeyChecking=accept-new` after `--`).
6. **The CI checkout fetches no tags** (`.gitea/workflows/check.yml`). `REPO_POLICIES.md` requires `fetch-depth: 0` on the checkout step of a repo that stamps a version from `git describe --tags`; this one does not set it. Acceptable: `with: fetch-depth: 0` on that step.
7. **Two claims in the PR body are not true** (https://git.eeqj.de/sneak/keyfunc/pulls/28). "The linter config and ignore files are the current templates": the `next` branch of `sneak/prompts` changed them and `REPO_POLICIES.md` on 2026-10-04 (golangci-lint v2.14.0 with `exhaustruct_v5`, `.git/modules/**/config`, the fix in item 3). "Every direct dependency is current": `github.com/btcsuite/btcd` stays at v0.25.0 while v0.26.2 is out, which moves `chaincfg` into a separate module. Acceptable: re-vendor those files along with item 3, or name the template date the branch carries; say why `btcd` is held.
Model: opus-5-5
The BIP-39 seed was computed over the mnemonic string as given, with only its ends trimmed, so the same words one per line, tab-separated or double-spaced passed the checksum but gave different keys with no warning. The words are now joined by single spaces before the checksum and the seed, for every source: the mnemonic command, both environment variables and the prompt. Single-spaced input gives the same keys as before; a test checks the README vector for each spacing.
Model: opus-5-5
The README now gives the child mnemonic keyfunc prints for the abandon ... about test mnemonic at index 0, checked by the README vectors test; the BIP-85 specification vector stays, marked as starting from a master key keyfunc cannot take. It also says ssh install needs the host key in known_hosts already, and how to get round that. ssh install now also lists ~/.ssh/. on its first connection and refuses, before any upload, a ~/.ssh it can read but not enter, which sftp shows as empty; a file where ~/.ssh belongs is refused the same way.
Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
Brings keyfunc to the current sneak/prompts templates: REPO_POLICIES.md and .golangci.yml are the template copies, the lint phase runs golangci-lint v2.14.0 on the template digest (its one new finding fixed), and .dockerignore gains the template line for submodule configs. The stage that compiles keyfunc marks /src safe for git, so a context sent as a tar stream still stamps the tag or short commit. The last stage is now a development environment, as the policy asks of a non-server repo: run the tool as docker run IMAGE keyfunc .... The CI checkout fetches tags.
Deviation: .gitea/workflows/check.yml differs from the template copy by fetch-depth: 0, which REPO_POLICIES.md requires.
Model: opus-5-5
SIGINT, SIGTERM and SIGHUP are no longer caught for the whole run, so they end any command at once, the mnemonic prompt included. One package, internal/cli/signals, catches them only where cleanup is needed, and only those not ignored at start, so nohup still works: ssh to and ssh install while their child runs, and age encrypt -o and age decrypt -o while they write. A signal received by the time the input ends leaves no new file and exits 1; otherwise the whole file is put in place, never an unfinished one.
Judgement call: the guarantee is stated for a signal keyfunc has received, as Go cannot promise more; the main goroutine stays on the main thread so a Ctrl-C on a pipeline is seen first on Linux. Unverified on macOS.
Model: opus-5-5 (implementation); fable-5-1 (design)
A signal during ssh install now sends sftp a SIGTERM instead of killing it, as ssh to already does for ssh, so sftp stops the ssh it started. The sftp command also has a WaitDelay of a quarter second, so a child that still holds sftp's output cannot keep the tool waiting on a host that does not answer: it ends with status 1 and removes its working directory. The test's sftp stand-in now leaves such a child.
Judgement call: exec.ErrWaitDelay counts as success, so a run that used ControlPersist does not report a failure for a key it added.
Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
age encrypt -o and age decrypt -o replace whatever is at the path (internal/cli/age/age.go, output and finish; README.md, keyfunc age encrypt). The output is always written to a new file beside the named path and then renamed over it. A symlink or a named pipe at that path is replaced by a regular file, and a reader of the pipe waits forever. -o /dev/stdout fails with "permission denied". Run as root, as in the development image, -o /dev/null replaces /dev/null with a 0600 file holding the ciphertext. An existing file also ends up with mode 0600, which the README does not say. Acceptable: rename only over a missing path or a regular file, and either refuse any other path before writing or write to it directly; the README says that an existing file is replaced and gets mode 0600; a test covers a symlink or a named pipe at the -o path.
Model: opus-5-5
1. **`age encrypt -o` and `age decrypt -o` replace whatever is at the path** (`internal/cli/age/age.go`, `output` and `finish`; `README.md`, `keyfunc age encrypt`). The output is always written to a new file beside the named path and then renamed over it. A symlink or a named pipe at that path is replaced by a regular file, and a reader of the pipe waits forever. `-o /dev/stdout` fails with "permission denied". Run as root, as in the development image, `-o /dev/null` replaces `/dev/null` with a `0600` file holding the ciphertext. An existing file also ends up with mode `0600`, which the README does not say. Acceptable: rename only over a missing path or a regular file, and either refuse any other path before writing or write to it directly; the README says that an existing file is replaced and gets mode `0600`; a test covers a symlink or a named pipe at the `-o` path.
Model: opus-5-5
age encrypt -o and age decrypt -o now look at the -o path before writing. A path that is the same file as the tool's standard output or standard error, under any name, is written to that stream, so a redirected file keeps its contents, inode and mode. A new path or a regular file is written beside it and renamed over it, as before, with the signal handling of #48. A symlink gets the same rule for what it points at, so the link survives; a dangling one is refused. A named pipe or a device is written directly. The README says a replaced file gets mode 0600.
Rule suppressed: gosec G304 on the direct open of the -o path.
Model: opus-5-5
ssh install gives sftp every word after the host, whether or not it follows -- (internal/cli/ssh/install.go, install: add(cmd, args[0], args[1:], line)). keyfunc ssh install alice@host frank@host puts frank@host in front of the host in sftp's arguments, so the key is installed for frank@host and the tool prints added. keyfunc ssh install host 2222 connects to a host called 2222. The README documents only install <[user@]host> [-- sftp options...]. Acceptable: an argument between the host and --, or a second argument when there is no --, is refused before any connection (cobra's ArgsLenAtDash), and a test covers it.
ssh install turns a symlinked ~/.ssh/authorized_keys into a regular file (internal/cli/ssh/install.go, upload; README.md, keyfunc ssh install). The rename replaces the link itself. The host ends up with a regular file holding the old lines plus the key, and the file the link pointed at never gets the key and is no longer used. The README does not say this, and age -o keeps a link. Acceptable: either the tool refuses such a link before any upload and says why, with a test, or the README says that a symlink at that path is replaced by a regular file.
The PR body's "Every other dependency is current" is not true (#28). go.mod requires github.com/spf13/pflag v1.0.9 (v1.0.10 is out) and github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 (v4.4.1 is out). Acceptable: the body says "every other direct dependency", or those two are updated.
Model: opus-5-5
1. **`ssh install` gives `sftp` every word after the host, whether or not it follows `--`** (`internal/cli/ssh/install.go`, `install`: `add(cmd, args[0], args[1:], line)`). `keyfunc ssh install alice@host frank@host` puts `frank@host` in front of the host in `sftp`'s arguments, so the key is installed for `frank@host` and the tool prints `added`. `keyfunc ssh install host 2222` connects to a host called `2222`. The README documents only `install <[user@]host> [-- sftp options...]`. Acceptable: an argument between the host and `--`, or a second argument when there is no `--`, is refused before any connection (cobra's `ArgsLenAtDash`), and a test covers it.
2. **`ssh install` turns a symlinked `~/.ssh/authorized_keys` into a regular file** (`internal/cli/ssh/install.go`, `upload`; `README.md`, `keyfunc ssh install`). The rename replaces the link itself. The host ends up with a regular file holding the old lines plus the key, and the file the link pointed at never gets the key and is no longer used. The README does not say this, and `age -o` keeps a link. Acceptable: either the tool refuses such a link before any upload and says why, with a test, or the README says that a symlink at that path is replaced by a regular file.
3. **The PR body's "Every other dependency is current" is not true** (https://git.eeqj.de/sneak/keyfunc/pulls/28). `go.mod` requires `github.com/spf13/pflag` v1.0.9 (v1.0.10 is out) and `github.com/decred/dcrd/dcrec/secp256k1/v4` v4.4.0 (v4.4.1 is out). Acceptable: the body says "every other direct dependency", or those two are updated.
Model: opus-5-5
ssh install now takes the host alone before --: any other word there, or a second argument without --, is refused before the mnemonic is read or sftp runs, so keyfunc ssh install alice@host frank@host no longer installs the key for frank@host. The first listing of ~/.ssh is now ls -n, which shows the file type, so a symlinked authorized_keys is refused before any upload instead of being replaced by a regular file; the README says so.
Judgement calls: install -- host is refused; a symlinked authorized_keys is refused even when its target already holds the key.
Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
The development image's /src is not a clean checkout (.dockerignore, keyfunc's own .gitea line). The image gets .git but not .gitea, so in it git status reports .gitea/workflows/check.yml as deleted, make build stamps a -dirty version, and git commit -a would delete the CI workflow. The README says the image holds the source tree in /src. Acceptable: .gitea removed from .dockerignore, so that git status in /src of the built image is empty and make build there stamps the short commit without -dirty.
The PR body says a symlinked ~/.ssh makes ssh install write nothing (#28, first point: "writes nothing when ~/.ssh or authorized_keys cannot be read or entered or is a symlink"). A ~/.ssh that is a symlink to a directory is followed, and the key is added to authorized_keys in that directory; only a symlinked authorized_keys is refused, as the README says. Acceptable: the body names the symlink refusal for authorized_keys alone.
Model: opus-5-5
1. **The development image's `/src` is not a clean checkout** (`.dockerignore`, keyfunc's own `.gitea` line). The image gets `.git` but not `.gitea`, so in it `git status` reports `.gitea/workflows/check.yml` as deleted, `make build` stamps a `-dirty` version, and `git commit -a` would delete the CI workflow. The README says the image holds the source tree in `/src`. Acceptable: `.gitea` removed from `.dockerignore`, so that `git status` in `/src` of the built image is empty and `make build` there stamps the short commit without `-dirty`.
2. **The PR body says a symlinked `~/.ssh` makes `ssh install` write nothing** (https://git.eeqj.de/sneak/keyfunc/pulls/28, first point: "writes nothing when `~/.ssh` or `authorized_keys` cannot be read or entered or is a symlink"). A `~/.ssh` that is a symlink to a directory is followed, and the key is added to `authorized_keys` in that directory; only a symlinked `authorized_keys` is refused, as the README says. Acceptable: the body names the symlink refusal for `authorized_keys` alone.
Model: opus-5-5
.dockerignore no longer keeps .gitea out of the build context. The development image gets .git, so without the workflow directory git status in /src reported the CI workflow as deleted, make build stamped a -dirty version, and git commit -a would have deleted the workflow. Now /src is a clean checkout and make build there stamps the short commit. The comment above /keyfunc now names only the binary.
Model: opus-5-5
package.json has no license field (package.json). Because of this, yarn install in script/bootstrap prints warning package.json: No license field and warning No license field in every image build (script/docker, script/cibuild, docker build .). The sneak/prompts template fixed exactly this on 2026-10-04 (sneak/prompts#76), before this branch took its templates of that date. Acceptable: package.json carries "license": "MIT" as the template does, and the bootstrap step of the image build prints no yarn warning.
Model: opus-5-5
1. **`package.json` has no `license` field** (`package.json`). Because of this, `yarn install` in `script/bootstrap` prints `warning package.json: No license field` and `warning No license field` in every image build (`script/docker`, `script/cibuild`, `docker build .`). The `sneak/prompts` template fixed exactly this on 2026-10-04 (https://git.eeqj.de/sneak/prompts/issues/76), before this branch took its templates of that date. Acceptable: `package.json` carries `"license": "MIT"` as the template does, and the bootstrap step of the image build prints no yarn warning.
Model: opus-5-5
package.json now carries "license": "MIT", as the sneak/prompts template does since 2026-10-04, so yarn install in script/bootstrap no longer warns about a missing license field on every image build.
Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
The ignore files leave out key files the template has ignored since 2026-10-04 (.gitignore, .dockerignore; the PR body's "follow the sneak/prompts templates of 2026-10-04"). Later on 2026-10-04 the next templates added id_ecdsa_sk and id_ed25519_sk to both files. These are the private key files ssh-keygen writes for hardware-backed keys (sneak/prompts#81). The .dockerignore template also now keeps out every .git/config (**/.git/config). keyfunc's copies are older, so git does not ignore such a key file in a clone, and COPY . . copies it into the development image's /src. REPO_POLICIES.md here has the same last_modified date as the canonical copy but older text. Acceptable: .gitignore and .dockerignore match the current next templates plus keyfunc's own /keyfunc lines, and REPO_POLICIES.md matches the current canonical copy, so that the PR body's claim is true.
Model: opus-5-5
1. **The ignore files leave out key files the template has ignored since 2026-10-04** (`.gitignore`, `.dockerignore`; the PR body's "follow the `sneak/prompts` templates of 2026-10-04"). Later on 2026-10-04 the `next` templates added `id_ecdsa_sk` and `id_ed25519_sk` to both files. These are the private key files `ssh-keygen` writes for hardware-backed keys (https://git.eeqj.de/sneak/prompts/issues/81). The `.dockerignore` template also now keeps out every `.git/config` (`**/.git/config`). keyfunc's copies are older, so git does not ignore such a key file in a clone, and `COPY . .` copies it into the development image's `/src`. `REPO_POLICIES.md` here has the same `last_modified` date as the canonical copy but older text. Acceptable: `.gitignore` and `.dockerignore` match the current `next` templates plus keyfunc's own `/keyfunc` lines, and `REPO_POLICIES.md` matches the current canonical copy, so that the PR body's claim is true.
Model: opus-5-5
Every file keyfunc copies from sneak/prompts now matches its next branch at commit dd4027b907ef99cdc3187c215cc4d610b7a11efc. REPO_POLICIES.md is the current copy; .gitignore and .dockerignore now ignore id_ecdsa_sk and id_ed25519_sk, the private key files ssh-keygen writes for hardware-backed keys, and .dockerignore keeps out a .git/config at any depth. The other copied files already matched; no adapted file needed a change.
Model: opus-5-5
merge-ready is off only until the Gitea check can run: next at 4471420 has passed an independent review of the whole branch, and every unit on it is reviewed. Every Actions job since about 18:44 UTC on 4 October stops at the checkout step with no space left on device on the runner fsn1app1 (the same outage raised on sneak/sfdupes#69). Needed from you: free disk space on fsn1app1, then re-run the check workflow on next (4471420). When it passes, this PR goes merge-ready.
Model: opus-5-5
`merge-ready` is off only until the Gitea check can run: `next` at `4471420` has passed an independent review of the whole branch, and every unit on it is reviewed. Every Actions job since about 18:44 UTC on 4 October stops at the checkout step with `no space left on device` on the runner `fsn1app1` (the same outage raised on https://git.eeqj.de/sneak/sfdupes/pulls/69). Needed from you: free disk space on `fsn1app1`, then re-run the `check` workflow on `next` (`4471420`). When it passes, this PR goes `merge-ready`.
Model: opus-5-5
go-bip39's repository no longer exists, so keyfunc now carries the
part of v1.1.0 it uses, with the upstream LICENSE beside it, and
imports it from internal/bip39. Upstream's tests and test vectors for
the kept code come along unchanged; where they called a removed
function, crypto/rand stands in for NewEntropy and EntropyFromMnemonic
for MnemonicToByteArray.
Changes beyond the trimming are what the linter asked for: the
package-level variables moved into the functions that use them, three
error strings were lower-cased, and the unknown-word error now wraps
ErrInvalidMnemonic.
go.mod no longer requires go-bip39. go mod tidy keeps its two go.sum
lines, because a test in sneak/secret's bip85 package imports it, and
drops six lines that only go-bip39's own requirements needed.
Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
sneak
merged commit 319630a684 into main2026-10-07 11:19:10 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What is on
next, all reviewed.ssh installworks oversftp, runs nothing on the host, and writes nothing when~/.sshorauthorized_keyscannot be read or entered, orauthorized_keysis a symlink (the defect of #12).sshandsftpchildren, and extra spaces or line breaks in it cannot change a key.sshcommands remove their working files first, and an interruptedage -oleaves no unfinished file.-okeeps a symlink, pipe, device or redirected output stream at its path.sneak.berlin/go/keyfunc, entrypointcmd/keyfunc;--versionshows the module version aftergo install, the tag or short commit fromdocker build ..internal/bip39, as you ruled; no derived key changes. Its twogo.sumlines stay while a test insneak/secret'sbip85imports it (sneak/secret#122).sneak/promptstemplates at commitdd4027b; the image's last stage is a development environment, andscript/cibuildneeds only Docker and git.To know before merging:
ssh installusessftpbatch mode: a key or an agent must authenticate, and the host key must already be known. Options after--go tosftp.go install sneak.berlin/go/keyfunc/cmd/keyfunc@latestresolves oncemaincarries the module move.btcdstays at v0.25.0: later releases drop packages thatsneak/secret'sbip85imports. Every other direct dependency is current.Model: opus-5-5
Signals now end only
ssh toandssh install(internal/cli/cli.go,Main). SIGINT, SIGTERM and SIGHUP are caught for every command, but only thesshandsftpchildren act on them. Ctrl-C at the mnemonic prompt does nothing until Enter is pressed, andage encryptandage decryptkeep reading after a signal: an interruptedproducer | keyfunc age encrypt -o fileputs a complete, decryptable encryption of the cut-off input in place and exits 0. Acceptable: every command ends promptly with a non-zero status on those signals (catch them only whilesshorsftpruns, or have the prompt and the age copy stop on the cancelled context), an interrupted-orun puts no file in place, and a test covers the age case.Whitespace inside the mnemonic silently changes every key (
internal/mnemonic/mnemonic.go,checked). Only the ends are trimmed. The same twelve words separated by two spaces, a tab, or one per line (as a--mnemonic-commandmay print them) pass the checksum, but the seed is computed over the string as given, so the keys differ from the README vectors with no warning. Acceptable: derive from the words joined by single spaces (or refuse any other spacing), with a test that one-word-per-line and double-spaced input give the README vectors.A build whose context is sent as a tar stream fails (
Dockerfile, build stage). Withdocker build - < context.taror the Docker Engine API, the files keep the sender's owner, git refuses/srcas dubious ownership, the version comes out empty and the version check stops the build. Acceptable:RUN git config --system --add safe.directory /srcin the build stage, as the canonical Dockerfile on thenextbranch ofsneak/promptsnow has.The README's child mnemonic vector cannot be checked with keyfunc (
README.md, Derived mnemonics;internal/cli/cli_test.go). The only vector starts from the BIP-85 specification's master key, which the tool cannot take, and no test pins whatkeyfunc mnemonicprints for a given mnemonic. Acceptable: theabandon … aboutchild mnemonic at index 0 next to the SSH and age vectors, asserted by a test that runs the command.ssh installalso needs the host key to be known already (README.md,keyfunc ssh install; the PR body's batch-mode note).sftpbatch mode refuses a host key that is not yet inknown_hosts, so a first run against a new host fails withHost key verification failed; the README names only the key-or-agent limit. Acceptable: say so, with the way round it (connect once withssh, or-o StrictHostKeyChecking=accept-newafter--).The CI checkout fetches no tags (
.gitea/workflows/check.yml).REPO_POLICIES.mdrequiresfetch-depth: 0on the checkout step of a repo that stamps a version fromgit describe --tags; this one does not set it. Acceptable:with: fetch-depth: 0on that step.Two claims in the PR body are not true (#28). "The linter config and ignore files are the current templates": the
nextbranch ofsneak/promptschanged them andREPO_POLICIES.mdon 2026-10-04 (golangci-lint v2.14.0 withexhaustruct_v5,.git/modules/**/config, the fix in item 3). "Every direct dependency is current":github.com/btcsuite/btcdstays at v0.25.0 while v0.26.2 is out, which moveschaincfginto a separate module. Acceptable: re-vendor those files along with item 3, or name the template date the branch carries; say whybtcdis held.Model: opus-5-5
clawbot referenced this pull request2026-10-04 06:44:05 +02:00
age encrypt -oandage decrypt -oreplace whatever is at the path (internal/cli/age/age.go,outputandfinish;README.md,keyfunc age encrypt). The output is always written to a new file beside the named path and then renamed over it. A symlink or a named pipe at that path is replaced by a regular file, and a reader of the pipe waits forever.-o /dev/stdoutfails with "permission denied". Run as root, as in the development image,-o /dev/nullreplaces/dev/nullwith a0600file holding the ciphertext. An existing file also ends up with mode0600, which the README does not say. Acceptable: rename only over a missing path or a regular file, and either refuse any other path before writing or write to it directly; the README says that an existing file is replaced and gets mode0600; a test covers a symlink or a named pipe at the-opath.Model: opus-5-5
ssh installgivessftpevery word after the host, whether or not it follows--(internal/cli/ssh/install.go,install:add(cmd, args[0], args[1:], line)).keyfunc ssh install alice@host frank@hostputsfrank@hostin front of the host insftp's arguments, so the key is installed forfrank@hostand the tool printsadded.keyfunc ssh install host 2222connects to a host called2222. The README documents onlyinstall <[user@]host> [-- sftp options...]. Acceptable: an argument between the host and--, or a second argument when there is no--, is refused before any connection (cobra'sArgsLenAtDash), and a test covers it.ssh installturns a symlinked~/.ssh/authorized_keysinto a regular file (internal/cli/ssh/install.go,upload;README.md,keyfunc ssh install). The rename replaces the link itself. The host ends up with a regular file holding the old lines plus the key, and the file the link pointed at never gets the key and is no longer used. The README does not say this, andage -okeeps a link. Acceptable: either the tool refuses such a link before any upload and says why, with a test, or the README says that a symlink at that path is replaced by a regular file.The PR body's "Every other dependency is current" is not true (#28).
go.modrequiresgithub.com/spf13/pflagv1.0.9 (v1.0.10 is out) andgithub.com/decred/dcrd/dcrec/secp256k1/v4v4.4.0 (v4.4.1 is out). Acceptable: the body says "every other direct dependency", or those two are updated.Model: opus-5-5
The development image's
/srcis not a clean checkout (.dockerignore, keyfunc's own.gitealine). The image gets.gitbut not.gitea, so in itgit statusreports.gitea/workflows/check.ymlas deleted,make buildstamps a-dirtyversion, andgit commit -awould delete the CI workflow. The README says the image holds the source tree in/src. Acceptable:.gitearemoved from.dockerignore, so thatgit statusin/srcof the built image is empty andmake buildthere stamps the short commit without-dirty.The PR body says a symlinked
~/.sshmakesssh installwrite nothing (#28, first point: "writes nothing when~/.sshorauthorized_keyscannot be read or entered or is a symlink"). A~/.sshthat is a symlink to a directory is followed, and the key is added toauthorized_keysin that directory; only a symlinkedauthorized_keysis refused, as the README says. Acceptable: the body names the symlink refusal forauthorized_keysalone.Model: opus-5-5
package.jsonhas nolicensefield (package.json). Because of this,yarn installinscript/bootstrapprintswarning package.json: No license fieldandwarning No license fieldin every image build (script/docker,script/cibuild,docker build .). Thesneak/promptstemplate fixed exactly this on 2026-10-04 (sneak/prompts#76), before this branch took its templates of that date. Acceptable:package.jsoncarries"license": "MIT"as the template does, and the bootstrap step of the image build prints no yarn warning.Model: opus-5-5
.gitignore,.dockerignore; the PR body's "follow thesneak/promptstemplates of 2026-10-04"). Later on 2026-10-04 thenexttemplates addedid_ecdsa_skandid_ed25519_skto both files. These are the private key filesssh-keygenwrites for hardware-backed keys (sneak/prompts#81). The.dockerignoretemplate also now keeps out every.git/config(**/.git/config). keyfunc's copies are older, so git does not ignore such a key file in a clone, andCOPY . .copies it into the development image's/src.REPO_POLICIES.mdhere has the samelast_modifieddate as the canonical copy but older text. Acceptable:.gitignoreand.dockerignorematch the currentnexttemplates plus keyfunc's own/keyfunclines, andREPO_POLICIES.mdmatches the current canonical copy, so that the PR body's claim is true.Model: opus-5-5
Review passed.
Model: opus-5-5
merge-readyis off only until the Gitea check can run:nextat4471420has passed an independent review of the whole branch, and every unit on it is reviewed. Every Actions job since about 18:44 UTC on 4 October stops at the checkout step withno space left on deviceon the runnerfsn1app1(the same outage raised on sneak/sfdupes#69). Needed from you: free disk space onfsn1app1, then re-run thecheckworkflow onnext(4471420). When it passes, this PR goesmerge-ready.Model: opus-5-5
don't worry about the server-side check. you can run "make check" yourself and trust the results.