From 91a5e9b0abe6bf7089940089608427ddc8ce5e73 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 04:00:41 +0000 Subject: [PATCH] Print every time in UTC, whatever the time zone (closes #169) secret list, secret version list, secret unlocker list and secret info printed times in the machine's time zone without naming it, so one vault showed different times on two machines. Entry() now sets time.Local to UTC before anything else, as AGENTS.md policy 6 asks, so every time the program prints or writes is in UTC: --json times end in Z, and a new version's name takes the UTC date. The README says so. A test runs the built secret list with TZ=America/New_York and checks the table and the JSON against the UTC time set on the secret's current file. Rule suppressed: gosmopolitan, which flags any use of time.Local. Model: opus-5-5 --- README.md | 4 +++ internal/cli/list_times_test.go | 54 +++++++++++++++++++++++++++++++++ internal/cli/root.go | 5 +++ 3 files changed, 63 insertions(+) diff --git a/README.md b/README.md index a62d1db..76f377a 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,10 @@ and unlocker set. This allows for complete isolation between different contexts ## Commands Reference +Every time `secret` prints, in a table or in `--json` output, is in UTC, +whatever `TZ` or the system's time zone is set to. The date in a version's name +is the UTC date too. + ### Confirmation Before Removal `secret rm`, `secret version rm`, `secret vault remove` and diff --git a/internal/cli/list_times_test.go b/internal/cli/list_times_test.go index 7bd87c5..43c0a3c 100644 --- a/internal/cli/list_times_test.go +++ b/internal/cli/list_times_test.go @@ -3,6 +3,9 @@ package cli_test import ( "bytes" "encoding/json" + "os" + "os/exec" + "path/filepath" "testing" "time" @@ -125,6 +128,57 @@ func TestMovedSecretKeepsItsTimes(t *testing.T) { "updated %s, then %s", beforeMove.UpdatedAt, afterMove.UpdatedAt) } +// TestListedTimesAreUTC is a regression test for +// https://git.eeqj.de/sneak/secret/issues/169, where times were printed +// in the local time zone. It runs `secret list` on the built binary with +// TZ set to New York, where the secret's last update, 03:04:05 UTC on 2 +// January, is 22:04:05 the day before. +func TestListedTimesAreUTC(t *testing.T) { + t.Parallel() + + stateDir := t.TempDir() + + vlt, err := vault.CreateVault( + afero.NewOsFs(), stateDir, "default", testMnemonicBuffer(t), nil) + require.NoError(t, err) + + vlt.Mnemonic = testMnemonicBuffer(t) + err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("one")), false) + require.NoError(t, err) + + updated := time.Date(2026, time.January, 2, 3, 4, 5, 0, time.UTC) + current := filepath.Join(stateDir, "vaults.d", "default", "secrets.d", "x", + "current") + require.NoError(t, os.Chtimes(current, updated, updated)) + + tests := []struct { + flags []string + want string + }{ + {flags: nil, want: "2026-01-02 03:04"}, + { + flags: []string{"--json"}, + want: `"updated_at": "2026-01-02T03:04:05Z"`, + }, + } + + for _, tt := range tests { + args := append([]string{"list"}, tt.flags...) + + //nolint:gosec // G204: test executes the freshly built secret binary + cmd := exec.CommandContext(t.Context(), secretBinaryPath(t), args...) + cmd.Env = []string{ + secret.EnvStateDir + "=" + stateDir, + "TZ=America/New_York", + "PATH=" + os.Getenv("PATH"), + } + + output, err := cmd.CombinedOutput() + require.NoError(t, err, "%v: %s", args, output) + assert.Contains(t, string(output), tt.want, "%v", args) + } +} + // listSecrets returns the output of `secret list`, or of // `secret list --json` when jsonOutput is set. func listSecrets(t *testing.T, c *cli.Instance, jsonOutput bool) string { diff --git a/internal/cli/root.go b/internal/cli/root.go index 65d1620..ee2e536 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -2,6 +2,7 @@ package cli import ( "os" + "time" "github.com/awnumar/memguard" "github.com/spf13/cobra" @@ -14,6 +15,10 @@ import ( // every memguard buffer before it returns, so the caller must do nothing // but exit with the code. func Entry() int { + // Every time secret prints or writes is in UTC, whatever TZ or the + // system's time zone is set to. + time.Local = time.UTC //nolint:gosmopolitan // this makes local time UTC + raiseLockedMemoryLimit() // On SIGINT or SIGTERM memguard runs this function, wipes every buffer