From e26912fdd6b7d755832af7d3348d49c4d92fed48 Mon Sep 17 00:00:00 2001 From: Scott Mabin Date: Tue, 28 Jul 2026 13:51:32 +0100 Subject: [PATCH] fix: Linux Chrome cookie extraction via fixed kooky keyring lookup Point kooky at a temporary replace of browserutils/kooky#116 so Linux Chrome can read Chrome Safe Storage from the default libsecret keyring (not only the login collection). That unblocks extract-token/check-token and uploads without GH_SESSION_TOKEN when the user is logged into GitHub in Chrome. Also add clear error hints when keyring lookup fails, and a short README note that interactive Linux use should prefer the native keyring path over injecting session cookies into agent environments. Drop the replace and bump to a released kooky version once browserutils/kooky#116 lands. --- README.md | 2 ++ go.mod | 2 ++ go.sum | 4 ++-- internal/cookies/cookies.go | 17 +++++++++++++++++ internal/cookies/cookies_test.go | 18 ++++++++++++++++++ 5 files changed, 41 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ece358..de25f1a 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,8 @@ The open [Agent Skills standard](https://agentskills.io/clients) is supported by On macOS, a Keychain prompt may appear on first use to authorize access to your browser's cookie encryption key. Click **Always Allow** to skip future prompts. +On Linux, Chrome cookie decryption needs the OS keyring (gnome-keyring / libsecret, or KWallet on KDE) unlocked so the library can read **Chrome Safe Storage**. Prefer that native path for interactive use; do not inject `user_session` / `GH_SESSION_TOKEN` into unattended agent environments. + > [!NOTE] > **When browser cookies aren't available:** Chrome 127+ on Windows isn't yet supported by the underlying cookie library ([workarounds](https://github.com/drogers0/gh-image/issues/4)), and Android (Termux) has no browser cookie store at all. In either case, supply the token explicitly via `GH_SESSION_TOKEN` (see [Session token override](#session-token-override) below); on Windows you can also just use another browser. diff --git a/go.mod b/go.mod index 773bd07..39ed30a 100644 --- a/go.mod +++ b/go.mod @@ -18,3 +18,5 @@ require ( golang.org/x/text v0.34.0 // indirect gopkg.in/ini.v1 v1.67.1 // indirect ) + +replace github.com/browserutils/kooky => github.com/MabezDev/kooky v0.2.11-0.20260728125024-3e00ef3ba597 diff --git a/go.sum b/go.sum index 112135b..649d18f 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ +github.com/MabezDev/kooky v0.2.11-0.20260728125024-3e00ef3ba597 h1:kZe2jEJNFHCK/kQLKatra4AuwriyfMiqfnJQo6J7dC8= +github.com/MabezDev/kooky v0.2.11-0.20260728125024-3e00ef3ba597/go.mod h1:ndMF+xzEi4voUsZ4dX0BL45oompbf5wqBaKR+J3lUNk= github.com/browserutils/ese v0.0.0-20260314233042-37b6a03a93ce h1:xb/LXUukZgVLMRnTUyEiCfMNH7KUCFOS4aOZnc/N+H8= github.com/browserutils/ese v0.0.0-20260314233042-37b6a03a93ce/go.mod h1:Rj9TJxm7cExxJmdec83sr8cjvyF3raBsszTFviSo/6U= -github.com/browserutils/kooky v0.2.10 h1:hEgbFJHf9lkMlSr0YdVEGtEo1yvqaTcGXNx0meNBgBA= -github.com/browserutils/kooky v0.2.10/go.mod h1:ndMF+xzEi4voUsZ4dX0BL45oompbf5wqBaKR+J3lUNk= github.com/browserutils/sqlite3 v0.0.2 h1:RDSivmwoS5DauKYxh06G4Y+m6IX3da7Cq9Jh5x+oIUA= github.com/browserutils/sqlite3 v0.0.2/go.mod h1:3i7CY1ba3/D+qovGRJyCgfDnu/lGc8sg8ieM6jIUO50= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/cookies/cookies.go b/internal/cookies/cookies.go index 45815f1..c1db8ac 100644 --- a/internal/cookies/cookies.go +++ b/internal/cookies/cookies.go @@ -81,6 +81,23 @@ var browserReadHints = []struct{ match, hint string }{ "(F12 > Application > Cookies > github.com > user_session) and set " + "GH_SESSION_TOKEN to it.", }, + { + // Surface keyring failures (libsecret / KWallet) with actionable recovery. + // Match is substring-based: kooky wraps these as + // "keyring password retrieval failed: …" (browserutils/kooky). + match: "keyring password retrieval failed", + hint: "Could not read the browser's cookie encryption key from the OS " + + "keyring. On Linux, ensure you are logged into GitHub in Chrome and " + + "that gnome-keyring (or KWallet on KDE) is unlocked. Confirm with: " + + "secret-tool lookup xdg:schema chrome_libsecret_os_crypt_password_v2. " + + "As a temporary override only, set GH_SESSION_TOKEN from DevTools.", + }, + { + match: "secret not found in keyring", + hint: "The Chrome Safe Storage secret was not found in the OS keyring. " + + "Log into Chrome so it creates \"Chrome Safe Storage\", unlock the " + + "keyring, and retry. Temporary override: GH_SESSION_TOKEN from DevTools.", + }, } // annotateReadError wraps a browser-read error as "reading browser cookies" and diff --git a/internal/cookies/cookies_test.go b/internal/cookies/cookies_test.go index c454f42..8fa908f 100644 --- a/internal/cookies/cookies_test.go +++ b/internal/cookies/cookies_test.go @@ -194,6 +194,24 @@ func TestAnnotateReadError(t *testing.T) { t.Errorf("expected bare wrap, got %q", got) } }) + + t.Run("keyring retrieval failure gets hint", func(t *testing.T) { + err := annotateReadError(fmt.Errorf("decrypting cookie: keyring password retrieval failed: kwallet: password not found")) + for _, want := range []string{"reading browser cookies", "keyring password retrieval failed", "hint:", "secret-tool", "GH_SESSION_TOKEN"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("missing %q in %v", want, err) + } + } + }) + + t.Run("secret not found in keyring gets hint", func(t *testing.T) { + err := annotateReadError(fmt.Errorf("keyring password retrieval failed: secret not found in keyring for application \"chrome\"")) + for _, want := range []string{"hint:", "Chrome Safe Storage", "GH_SESSION_TOKEN"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("missing %q in %v", want, err) + } + } + }) } func TestChooseSession(t *testing.T) {