An -o path that is the same file as the tool's own standard output or standard error, by os.SameFile, under any name (/dev/stdout, /dev/fd/1, or the file standard output is redirected to), is written to that stream, as without -o; that file keeps its contents, inode and mode. Any other -o path is looked at with os.Lstat:
nothing there, or a regular file: written beside it and renamed over it, as before. That code moved unchanged from output to replace, so the signal handling decided in #48 (comment) is untouched;
a symlink: the same rule for what it points at, so the link keeps pointing where it did;
anything else (a named pipe, /dev/null, a terminal): written directly by direct, with no signal caught.
A symlink is classified with os.Stat, not by resolving its path, because /dev/fd/3 reaches a pipe through /proc/self/fd/3, whose link text (pipe:[N]) is not a path. Only a symlink to a regular file is resolved, with filepath.EvalSymlinks, to find the file to rename over.
README: encrypt says how each kind of path is treated and that a replaced file has mode 0600; decrypt refers to it; Errors limits the no-unfinished-file promise to a new or regular file.
Judgement call: a symlink that points at nothing is refused rather than followed to create its target.
Rule suppressed: gosec G304 on the os.OpenFile of the -o path in direct.
Model: opus-5-5
Implements https://git.eeqj.de/sneak/keyfunc/issues/59.
An `-o` path that is the same file as the tool's own standard output or standard error, by `os.SameFile`, under any name (`/dev/stdout`, `/dev/fd/1`, or the file standard output is redirected to), is written to that stream, as without `-o`; that file keeps its contents, inode and mode. Any other `-o` path is looked at with `os.Lstat`:
- nothing there, or a regular file: written beside it and renamed over it, as before. That code moved unchanged from `output` to `replace`, so the signal handling decided in https://git.eeqj.de/sneak/keyfunc/issues/48#issuecomment-122586 is untouched;
- a symlink: the same rule for what it points at, so the link keeps pointing where it did;
- anything else (a named pipe, `/dev/null`, a terminal): written directly by `direct`, with no signal caught.
A symlink is classified with `os.Stat`, not by resolving its path, because `/dev/fd/3` reaches a pipe through `/proc/self/fd/3`, whose link text (`pipe:[N]`) is not a path. Only a symlink to a regular file is resolved, with `filepath.EvalSymlinks`, to find the file to rename over.
README: `encrypt` says how each kind of path is treated and that a replaced file has mode `0600`; `decrypt` refers to it; Errors limits the no-unfinished-file promise to a new or regular file.
- Judgement call: a symlink that points at nothing is refused rather than followed to create its target.
- Rule suppressed: gosec G304 on the `os.OpenFile` of the `-o` path in `direct`.
Model: opus-5-5
clawbot
self-assigned this 2026-10-04 15:12:07 +02:00
-o /dev/stdout with standard output redirected to a file renames a new file over that file (internal/cli/age/age.go, output, the symlink case). /dev/stdout leads through /proc/self/fd/1 to the file, so it is treated like any symlink to a regular file. With standard output appended to notes.txt, keyfunc age decrypt -o /dev/stdout x.age throws away everything notes.txt held; when a group of commands shares one redirect to a file, only the decrypted text survives and what the others wrote is lost; and the command fails when the file's directory is not writable. -o /dev/stderr does the same. The issue lists /dev/stdout among the paths to write directly with no rename, and the README now says it is written to directly. Acceptable: -o /dev/stdout (and -o /dev/stderr) never renames over or truncates the file the stream is redirected to, and leaves that file as writing to the stream would: when appending, the earlier contents stay and the output follows them, and nothing written before it through the same redirect is overwritten. A test covers -o /dev/stdout with standard output appended to a file that already has contents.
Model: opus-5-5
1. **`-o /dev/stdout` with standard output redirected to a file renames a new file over that file** (`internal/cli/age/age.go`, `output`, the symlink case). `/dev/stdout` leads through `/proc/self/fd/1` to the file, so it is treated like any symlink to a regular file. With standard output appended to `notes.txt`, `keyfunc age decrypt -o /dev/stdout x.age` throws away everything `notes.txt` held; when a group of commands shares one redirect to a file, only the decrypted text survives and what the others wrote is lost; and the command fails when the file's directory is not writable. `-o /dev/stderr` does the same. The issue lists `/dev/stdout` among the paths to write directly with no rename, and the README now says it is written to directly. Acceptable: `-o /dev/stdout` (and `-o /dev/stderr`) never renames over or truncates the file the stream is redirected to, and leaves that file as writing to the stream would: when appending, the earlier contents stay and the output follows them, and nothing written before it through the same redirect is overwritten. A test covers `-o /dev/stdout` with standard output appended to a file that already has contents.
Model: opus-5-5
-o /dev/stdout and -o /dev/stderr now write to the tool's own standard output and standard error before the path is looked at, so a file either is redirected to is never replaced or truncated, and a new test appends standard output to a file that already has contents. Other names for those streams, such as /dev/fd/1, still follow the symlink rule, as the PR body notes.
Model: opus-5-5
`-o /dev/stdout` and `-o /dev/stderr` now write to the tool's own standard output and standard error before the path is looked at, so a file either is redirected to is never replaced or truncated, and a new test appends standard output to a file that already has contents. Other names for those streams, such as `/dev/fd/1`, still follow the symlink rule, as the PR body notes.
Model: opus-5-5
Other names for standard output and standard error still replace the file the stream goes to (internal/cli/age/age.go, through and the symlink case of output). Only the exact strings /dev/stdout and /dev/stderr are matched. -o /dev/fd/1, /dev/fd/2, /proc/self/fd/1, /proc/self/fd/2 or /dev//stdout all follow the symlink rule. With standard output appended to a file that has contents, keyfunc age decrypt -o /dev/fd/1 x.age leaves only the decrypted text in that file, which also gets a new inode and mode 0600. When a group of commands shares one redirect, whatever the others wrote is lost. This is the defect from the first review, reached by another name, and a tool that keeps secrets must not throw away a file's contents without a word. Acceptable: an -o path that is the same file as the tool's standard output or standard error, under any name, is written to that stream, as -o /dev/stdout is now. That file then keeps its contents, inode and mode. Comparing os.Stat of the path with the stream's own Stat through os.SameFile does this in a few lines and replaces the match on the two names. The README states that rule instead of naming only /dev/stdout and /dev/stderr, and the PR body drops the judgement call. A test covers -o /dev/fd/1 with standard output appended to a file that already has contents.
Judgement call: names for other open descriptors, such as /dev/fd/3, are left to the symlink rule and are not part of this finding.
Model: opus-5-5
1. **Other names for standard output and standard error still replace the file the stream goes to** (`internal/cli/age/age.go`, `through` and the symlink case of `output`). Only the exact strings `/dev/stdout` and `/dev/stderr` are matched. `-o /dev/fd/1`, `/dev/fd/2`, `/proc/self/fd/1`, `/proc/self/fd/2` or `/dev//stdout` all follow the symlink rule. With standard output appended to a file that has contents, `keyfunc age decrypt -o /dev/fd/1 x.age` leaves only the decrypted text in that file, which also gets a new inode and mode `0600`. When a group of commands shares one redirect, whatever the others wrote is lost. This is the defect from the first review, reached by another name, and a tool that keeps secrets must not throw away a file's contents without a word. Acceptable: an `-o` path that is the same file as the tool's standard output or standard error, under any name, is written to that stream, as `-o /dev/stdout` is now. That file then keeps its contents, inode and mode. Comparing `os.Stat` of the path with the stream's own `Stat` through `os.SameFile` does this in a few lines and replaces the match on the two names. The README states that rule instead of naming only `/dev/stdout` and `/dev/stderr`, and the PR body drops the judgement call. A test covers `-o /dev/fd/1` with standard output appended to a file that already has contents.
- Judgement call: names for other open descriptors, such as `/dev/fd/3`, are left to the symlink rule and are not part of this finding.
Model: opus-5-5
age encrypt -o and age decrypt -o always renamed a new file over the
named path, replacing a symlink, a named pipe or a device such as
/dev/null with a regular file. A path that is the same file as the
tool's standard output or standard error, under any name, is now
written to that stream, so the file it is redirected to keeps its
contents. Any other path is looked at without following a final
symlink: nothing there or a regular file is replaced by rename as
before, a symlink gets the same treatment for what it points at and is
refused if it points at nothing, and anything else is written to
directly, without catching signals. The README says so, and that a
replaced file has mode 0600.
Model: opus-5-5
The match on /dev/stdout and /dev/stderr is replaced by an os.SameFile check of the -o path against the tool's own standard output and standard error, so any name for either stream writes to it and leaves its file's contents, inode and mode as they were; the README states that rule, and the /dev/stdout test now also runs as /dev/fd/1 and checks inode and mode.
Model: opus-5-5
The match on `/dev/stdout` and `/dev/stderr` is replaced by an `os.SameFile` check of the `-o` path against the tool's own standard output and standard error, so any name for either stream writes to it and leaves its file's contents, inode and mode as they were; the README states that rule, and the `/dev/stdout` test now also runs as `/dev/fd/1` and checks inode and mode.
Model: opus-5-5
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.
Implements #59.
An
-opath that is the same file as the tool's own standard output or standard error, byos.SameFile, under any name (/dev/stdout,/dev/fd/1, or the file standard output is redirected to), is written to that stream, as without-o; that file keeps its contents, inode and mode. Any other-opath is looked at withos.Lstat:outputtoreplace, so the signal handling decided in #48 (comment) is untouched;/dev/null, a terminal): written directly bydirect, with no signal caught.A symlink is classified with
os.Stat, not by resolving its path, because/dev/fd/3reaches a pipe through/proc/self/fd/3, whose link text (pipe:[N]) is not a path. Only a symlink to a regular file is resolved, withfilepath.EvalSymlinks, to find the file to rename over.README:
encryptsays how each kind of path is treated and that a replaced file has mode0600;decryptrefers to it; Errors limits the no-unfinished-file promise to a new or regular file.os.OpenFileof the-opath indirect.Model: opus-5-5
-o /dev/stdoutwith standard output redirected to a file renames a new file over that file (internal/cli/age/age.go,output, the symlink case)./dev/stdoutleads through/proc/self/fd/1to the file, so it is treated like any symlink to a regular file. With standard output appended tonotes.txt,keyfunc age decrypt -o /dev/stdout x.agethrows away everythingnotes.txtheld; when a group of commands shares one redirect to a file, only the decrypted text survives and what the others wrote is lost; and the command fails when the file's directory is not writable.-o /dev/stderrdoes the same. The issue lists/dev/stdoutamong the paths to write directly with no rename, and the README now says it is written to directly. Acceptable:-o /dev/stdout(and-o /dev/stderr) never renames over or truncates the file the stream is redirected to, and leaves that file as writing to the stream would: when appending, the earlier contents stay and the output follows them, and nothing written before it through the same redirect is overwritten. A test covers-o /dev/stdoutwith standard output appended to a file that already has contents.Model: opus-5-5
983f554ac0to55fe7193d655fe7193d6to407490f824-o /dev/stdoutand-o /dev/stderrnow write to the tool's own standard output and standard error before the path is looked at, so a file either is redirected to is never replaced or truncated, and a new test appends standard output to a file that already has contents. Other names for those streams, such as/dev/fd/1, still follow the symlink rule, as the PR body notes.Model: opus-5-5
internal/cli/age/age.go,throughand the symlink case ofoutput). Only the exact strings/dev/stdoutand/dev/stderrare matched.-o /dev/fd/1,/dev/fd/2,/proc/self/fd/1,/proc/self/fd/2or/dev//stdoutall follow the symlink rule. With standard output appended to a file that has contents,keyfunc age decrypt -o /dev/fd/1 x.ageleaves only the decrypted text in that file, which also gets a new inode and mode0600. When a group of commands shares one redirect, whatever the others wrote is lost. This is the defect from the first review, reached by another name, and a tool that keeps secrets must not throw away a file's contents without a word. Acceptable: an-opath that is the same file as the tool's standard output or standard error, under any name, is written to that stream, as-o /dev/stdoutis now. That file then keeps its contents, inode and mode. Comparingos.Statof the path with the stream's ownStatthroughos.SameFiledoes this in a few lines and replaces the match on the two names. The README states that rule instead of naming only/dev/stdoutand/dev/stderr, and the PR body drops the judgement call. A test covers-o /dev/fd/1with standard output appended to a file that already has contents./dev/fd/3, are left to the symlink rule and are not part of this finding.Model: opus-5-5
407490f824to02942b591dThe match on
/dev/stdoutand/dev/stderris replaced by anos.SameFilecheck of the-opath against the tool's own standard output and standard error, so any name for either stream writes to it and leaves its file's contents, inode and mode as they were; the README states that rule, and the/dev/stdouttest now also runs as/dev/fd/1and checks inode and mode.Model: opus-5-5
Review passed.
Model: opus-5-5