Skip to content

Commit 8a19418

Browse files
committed
feat(auth): make auto-detect the default for x auth import
Previously you had to pass --from-browser chrome to get the browser cookie auto-import. That was hidden UX. The right default is: just work. `x auth import` (no flags) now: 1. Scans every local browser cookie store (Chrome, Firefox, Brave, Edge, Chromium) via kooky. 2. Decrypts the values using the per-OS Safe Storage key. 3. Takes the first browser that has an x.com session. 4. Saves to keychain and runs verify (twid → UserByRestId). If no browser has a logged-in x.com session — headless container, fresh machine, all browsers locked — x-cli SOFT-FAILS to an interactive paste prompt with a single info line. No error, no choice menu, no "now try this with --from-browser". Override flags exist but are explicit: --from-browser <name> pin a specific browser instead of auto --paste skip auto-detect, prompt --cookie '...' one-shot for scripted setups cmd/auth.go: rewrote acquireCookieString in priority order --cookie | --paste | --from-browser | (default = auto+fallback). New readBrowserCookies helper with a softMiss flag so the default path can swallow "no cookies found" errors and fall through. promptCookiePaste extracted for clarity. README.md and skills/x-cli/{SKILL.md,references/auth.md} updated to lead with the bare `x auth import` and demote --from-browser to an override for edge cases.
1 parent 5020ced commit 8a19418

4 files changed

Lines changed: 138 additions & 112 deletions

File tree

README.md

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,42 +41,40 @@ make build
4141

4242
## Auth
4343

44-
Three ways to import the session:
45-
46-
**1. Auto from a local browser (recommended)**
47-
4844
```
49-
x auth import --from-browser chrome
50-
x auth import --from-browser firefox
51-
x auth import --from-browser brave
52-
x auth import --from-browser edge
45+
x auth import
5346
```
5447

55-
x-cli reads the browser's cookie SQLite store on disk and decrypts
56-
the values using the per-OS Safe Storage key (macOS Keychain on Mac,
57-
libsecret/kwallet on Linux, DPAPI on Windows). Same mechanism Python's
58-
`browser_cookie3` and `rookiepy` use. Chrome must be **closed** on macOS
59-
because it locks the cookie file; Firefox and Linux Chrome usually work
60-
while open. macOS will prompt once for Keychain access on the first run.
48+
That's it. `x auth import` auto-detects a logged-in x.com session in
49+
any local browser (Chrome, Firefox, Brave, Edge, Chromium), reads the
50+
cookie store directly from disk, and decrypts the values using the
51+
per-OS Safe Storage key (macOS Keychain on Mac, libsecret/kwallet on
52+
Linux, DPAPI on Windows). Same mechanism Python's `browser_cookie3` and
53+
`rookiepy` use. No flags, no DevTools paste.
6154

62-
**2. Manual paste**
55+
If auto-detect finds nothing (headless container, fresh machine, all
56+
browsers closed and locked), x-cli falls through to an interactive
57+
paste prompt automatically — open x.com → DevTools → Application →
58+
Cookies → copy `auth_token` + `ct0` → paste at the prompt.
6359

64-
Open x.com in your real browser, DevTools → Application → Cookies, copy
65-
`auth_token` and `ct0`, then:
60+
**Per-OS notes:**
6661

67-
```
68-
x auth import
69-
# paste: auth_token=...; ct0=...; twid=u%3D...
70-
```
62+
- macOS prompts once for Keychain access on the first run so we can
63+
read the Chrome Safe Storage AES key. The system dialog says "x
64+
wants to access key 'Chrome' in your keychain" — that's normal.
65+
- Chrome must be **closed** on macOS because it holds an exclusive
66+
lock on the cookie file while running. Firefox is fine while open.
67+
- Linux Chrome needs `libsecret` or `kwallet` running.
68+
- Windows uses DPAPI; works with the browser running.
7169

72-
**3. Scripted (CI / setup scripts)**
70+
**Override only if you need to:**
7371

7472
```
75-
x auth import --cookie 'auth_token=...; ct0=...; twid=u%3D...'
73+
x auth import --from-browser chrome # pin a specific browser
74+
x auth import --paste # force the paste prompt
75+
x auth import --cookie 'auth_token=...' # scripted setups
7676
```
7777

78-
(Visible in shell history — prefer `--from-browser` for normal use.)
79-
8078
**Where the cookie lives.** x-cli tries the OS keychain first (`go-keyring`:
8179
Keychain on macOS, libsecret on Linux, Credential Manager on Windows). If the
8280
keychain is unavailable — headless boxes, containers, CI, Linux without a

cmd/auth.go

Lines changed: 88 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
var (
1919
authFromBrowser string
2020
authCookieFlag string
21+
authForcePaste bool
2122
)
2223

2324
var authCmd = &cobra.Command{
@@ -27,41 +28,33 @@ var authCmd = &cobra.Command{
2728

2829
var authImportCmd = &cobra.Command{
2930
Use: "import",
30-
Short: "Import session cookies from your browser",
31+
Short: "Import session cookies from your browser (auto)",
3132
Long: `Import the auth_token and ct0 cookies from an X session.
3233
33-
Three import modes:
34+
By default, `+"`x auth import`"+` auto-detects: it scans every local
35+
browser's cookie store on disk (Chrome, Firefox, Brave, Edge, Chromium),
36+
decrypts the encrypted values with the per-OS Safe Storage key, and
37+
uses whichever browser has a live x.com session. No flags, no paste.
3438
35-
1. Auto from a local browser (no manual paste — recommended)
39+
Override only if you need to:
3640
37-
x auth import --from-browser chrome
38-
x auth import --from-browser firefox
39-
x auth import --from-browser brave
40-
x auth import --from-browser edge
41+
--from-browser chrome pin a specific browser instead of auto
42+
--cookie 'auth_token=...; ct0=...' one-shot from a flag (scripted)
43+
--paste force the interactive paste prompt
4144
42-
Reads the cookie store directly from disk and decrypts the values
43-
using the browser's per-OS Safe Storage key. macOS may prompt
44-
once for Keychain access. Chrome must be CLOSED on macOS — it
45-
locks the cookie file while running. Firefox usually works while
46-
open.
45+
Notes:
4746
48-
2. One-shot from a flag (scripted setups)
47+
- macOS prompts once for Keychain access on the first run so we can
48+
read the Chrome Safe Storage AES key. The system dialog says "x
49+
wants to access key 'Chrome' in your keychain" — that's normal.
50+
- Chrome must be CLOSED on macOS because it holds an exclusive lock
51+
on the cookie file while running. Firefox is fine while open.
52+
- If auto-detect finds no x.com session in any browser (typical in a
53+
headless container or fresh machine), x-cli falls back to the
54+
interactive paste prompt automatically.
4955
50-
x auth import --cookie 'auth_token=...; ct0=...; twid=u%3D...'
51-
52-
Pass the full cookie header on the command line. Visible in
53-
shell history; prefer --from-browser or stdin paste for normal use.
54-
55-
3. Interactive paste (default — works everywhere)
56-
57-
x auth import
58-
# paste at the prompt: auth_token=...; ct0=...; twid=u%3D...
59-
60-
Open x.com in your real browser, DevTools → Application → Cookies
61-
→ https://x.com, copy auth_token + ct0, paste here.
62-
63-
In all three modes, x-cli stores the cookies in your OS keychain via
64-
go-keyring. They are never written to disk in plaintext.`,
56+
x-cli stores cookies in your OS keychain via go-keyring. They are
57+
never written to disk in plaintext.`,
6558
RunE: runAuthImport,
6659
}
6760

@@ -79,9 +72,11 @@ var authLogoutCmd = &cobra.Command{
7972

8073
func init() {
8174
authImportCmd.Flags().StringVar(&authFromBrowser, "from-browser", "",
82-
"read cookies directly from a local browser: chrome | firefox | brave | edge | chromium")
75+
"pin to a specific browser: chrome | firefox | brave | edge | chromium")
8376
authImportCmd.Flags().StringVar(&authCookieFlag, "cookie", "",
84-
"non-interactive: pass the full cookie header as a flag (visible in shell history)")
77+
"non-interactive: pass the full cookie header (visible in shell history)")
78+
authImportCmd.Flags().BoolVar(&authForcePaste, "paste", false,
79+
"skip auto-detect and prompt for an interactive paste")
8580

8681
authCmd.AddCommand(authImportCmd)
8782
authCmd.AddCommand(authStatusCmd)
@@ -151,32 +146,77 @@ func runAuthImport(cmd *cobra.Command, _ []string) error {
151146
return nil
152147
}
153148

154-
// acquireCookieString returns the cookie header string from one of the
155-
// three import modes (--from-browser, --cookie, interactive paste).
156-
// The three modes are mutually exclusive and checked in priority order.
149+
// acquireCookieString resolves the cookie string in priority order:
150+
//
151+
// 1. --cookie '...' (explicit one-shot)
152+
// 2. --from-browser <name> (explicit browser pin)
153+
// 3. --paste (explicit interactive paste)
154+
// 4. (default) auto-scan all browsers, fall through to paste if empty
155+
//
156+
// The default mode is the only one that can SOFT-FAIL: if no browser
157+
// has an x.com session, we silently drop to the paste prompt without
158+
// error. Explicit modes hard-fail on their own terms.
157159
func acquireCookieString() (string, error) {
158160
switch {
161+
case authCookieFlag != "":
162+
return strings.TrimSpace(authCookieFlag), nil
163+
164+
case authForcePaste:
165+
return promptCookiePaste()
166+
159167
case authFromBrowser != "":
160-
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
161-
defer cancel()
162-
res, err := browsercookies.Load(ctx, authFromBrowser, "x.com")
163-
if err != nil {
164-
return "", fmt.Errorf("--from-browser %s: %w", authFromBrowser, err)
168+
raw, _, err := readBrowserCookies(authFromBrowser, false)
169+
return raw, err
170+
171+
default:
172+
// Auto-detect: any browser. Falls through to paste on miss.
173+
raw, source, err := readBrowserCookies("", true)
174+
if err == nil && raw != "" {
175+
cmdutil.Success("auto-detected x.com session in %s", source)
176+
return raw, nil
165177
}
166-
raw := browsercookies.FormatCookieHeader(res.Cookies, cookieNamesWanted)
167-
if raw == "" {
168-
return "", fmt.Errorf("--from-browser %s: required cookies (auth_token, ct0) not found", authFromBrowser)
178+
if err != nil {
179+
cmdutil.Warn("auto-detect: %v", err)
169180
}
170-
cmdutil.Info("loaded %d x.com cookie(s) from %s (%s)",
171-
countCookies(res.Cookies, cookieNamesWanted), res.Browser, res.Source)
172-
return raw, nil
181+
cmdutil.Info("falling back to interactive paste (use --from-browser to pin a specific browser)")
182+
return promptCookiePaste()
183+
}
184+
}
173185

174-
case authCookieFlag != "":
175-
return strings.TrimSpace(authCookieFlag), nil
186+
// readBrowserCookies runs kooky against the named browser (or all
187+
// browsers when name == ""). On success returns the formatted cookie
188+
// header and a friendly source description. On no-cookies-found returns
189+
// an empty string and a non-nil error if `hardError` is false (caller
190+
// will treat as soft miss); when `hardError` is true the error is
191+
// surfaced verbatim.
192+
func readBrowserCookies(browser string, softMiss bool) (cookieHeader, source string, err error) {
193+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
194+
defer cancel()
176195

177-
default:
178-
return cmdutil.ReadSecret("Paste cookie header (auth_token=...; ct0=...): ")
196+
res, err := browsercookies.Load(ctx, browser, "x.com")
197+
if err != nil {
198+
if softMiss {
199+
return "", "", err
200+
}
201+
if browser != "" {
202+
return "", "", fmt.Errorf("--from-browser %s: %w", browser, err)
203+
}
204+
return "", "", err
179205
}
206+
207+
raw := browsercookies.FormatCookieHeader(res.Cookies, cookieNamesWanted)
208+
if raw == "" {
209+
miss := fmt.Errorf("required cookies (auth_token, ct0) not in %s store — are you logged in?", res.Browser)
210+
if softMiss {
211+
return "", "", miss
212+
}
213+
return "", "", miss
214+
}
215+
return raw, fmt.Sprintf("%s (%s)", res.Browser, res.Source), nil
216+
}
217+
218+
func promptCookiePaste() (string, error) {
219+
return cmdutil.ReadSecret("Paste cookie header (auth_token=...; ct0=...): ")
180220
}
181221

182222
// countCookies returns how many of the wanted names are present in the

skills/x-cli/SKILL.md

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ description: >
1414
```
1515
x
1616
├── auth
17-
│ ├── import # --from-browser chrome|firefox|brave|edge | --cookie '...' | interactive paste
17+
│ ├── import # auto-detect any local browser (default); --from-browser, --paste, --cookie override
1818
│ ├── status # twid → UserByRestId self-lookup
1919
│ └── logout # remove stored session
2020
├── doctor # endpoints, session, egress IP, ASN check
@@ -52,18 +52,13 @@ reports a cloud ASN for your egress IP, do not run `x grow` — your session
5252
normally logs in from residential and X will flag that asymmetry.
5353

5454
```bash
55-
# Easiest: auto-read from a local browser (Chrome must be closed on macOS)
56-
x auth import --from-browser chrome
57-
x auth import --from-browser firefox # works while Firefox is running
58-
x auth import --from-browser brave
55+
x auth import # auto-detect any local browser, fall through to paste on miss
56+
x doctor # expect: endpoints ok, session ok, egress not-cloud
5957

60-
# Or: paste the cookie header manually
61-
x auth import # prompts; paste: auth_token=...; ct0=...; twid=u%3D...
62-
63-
# Or: scripted setups
58+
# Override only if you need to pin or script:
59+
x auth import --from-browser firefox
60+
x auth import --paste
6461
x auth import --cookie 'auth_token=...; ct0=...; twid=u%3D...'
65-
66-
x doctor # expect: endpoints ok, session ok, egress not-cloud
6762
```
6863

6964
## Read-only scraping

skills/x-cli/references/auth.md

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,20 @@ for sanity checks but not required for the request to succeed.
1818

1919
## Import flow
2020

21-
Three modes, in order of how quickly you can get going:
22-
23-
### 1. Auto from a local browser (recommended)
24-
2521
```
26-
x auth import --from-browser chrome # must be closed on macOS
27-
x auth import --from-browser firefox # works while running
28-
x auth import --from-browser brave
29-
x auth import --from-browser edge
30-
x auth import --from-browser chromium
22+
x auth import
3123
```
3224

33-
x-cli reads the browser's cookie SQLite store on disk and decrypts the
34-
encrypted values using the OS-specific Safe Storage key. This is exactly
35-
what Python's `browser_cookie3` / `rookiepy` libraries do, ported to Go
36-
via `github.com/browserutils/kooky`. You log in once in a real browser;
37-
x-cli uses the live session.
25+
That's the whole flow. `x auth import` auto-detects a logged-in x.com
26+
session in **any** local browser, reads the cookie SQLite store on
27+
disk, and decrypts the encrypted values using the OS-specific Safe
28+
Storage key. Same primitive Python's `browser_cookie3` / `rookiepy`
29+
expose, ported to Go via `github.com/browserutils/kooky`.
30+
31+
Default behaviour: scan Chrome → Firefox → Brave → Edge → Chromium,
32+
take the first one that has an x.com session, save to keychain, done.
33+
If nothing is found (headless container, fresh machine, all browsers
34+
locked), x-cli silently drops to an interactive paste prompt.
3835

3936
Per-OS notes:
4037

@@ -43,27 +40,23 @@ Per-OS notes:
4340
for Chrome's Safe Storage lives in your keychain. **Chrome must be
4441
closed** because it holds an exclusive lock on the cookie file.
4542
Firefox is fine while running.
46-
- **Linux**: needs `libsecret` or `kwallet` running for Chrome family.
47-
Falls back to a hardcoded "peanuts" salt on truly headless hosts.
48-
Firefox needs no daemon.
43+
- **Linux**: needs `libsecret` or `kwallet` running for the Chrome
44+
family. Falls back to a hardcoded `peanuts` salt on truly headless
45+
hosts. Firefox needs no daemon.
4946
- **Windows**: uses DPAPI; works while the browser is running.
5047

51-
### 2. Manual paste (works everywhere, including headless containers)
52-
53-
1. Log into x.com in a real browser on a machine you normally use.
54-
2. DevTools → Application → Cookies → `https://x.com`
55-
3. Copy the values for `auth_token` and `ct0`.
56-
4. `x auth import` → paste `auth_token=...; ct0=...; twid=u%3D...`
57-
5. `x auth status` should print `session ok — @yourhandle`.
48+
### Override modes
5849

59-
### 3. Scripted (`--cookie`)
50+
Use these only when auto-detect is wrong for your case:
6051

6152
```
62-
x auth import --cookie 'auth_token=...; ct0=...; twid=u%3D...'
53+
x auth import --from-browser firefox # pin a specific browser
54+
x auth import --paste # skip auto-detect, prompt
55+
x auth import --cookie 'auth_token=...' # scripted bootstrap
6356
```
6457

65-
The cookie ends up in your shell history — prefer one of the other
66-
modes for normal use. Useful for CI bootstrap scripts.
58+
`--cookie` ends up in shell history — prefer `--from-browser` or the
59+
default for normal use.
6760

6861
### What happens after import
6962

0 commit comments

Comments
 (0)