Summary
On macOS, option 1 (browser session cookies) in login_setup.py can never succeed. The script hangs forever at the Paste the Cookie header value: prompt, and Enter does nothing.
The cause is a hard tty limit, not a bad cookie or a slow network call.
Cause
login_setup.py:42 reads the cookie with getpass.getpass():
cookie_string = getpass.getpass("Paste the Cookie header value: ").strip()
getpass reads a line from /dev/tty in canonical mode. Canonical-mode input is capped at MAX_CANON bytes per line, and on Darwin that is 1024. A real Monarch Cookie header is several KB (Segment ajs_*, Osano consent, Cloudflare, plus the auth cookies), so the line never terminates: the terminal emits BEL for every byte past the limit and discards it. readline() blocks indefinitely.
Linux sets MAX_CANON to 4096, which is probably why this hasn't shown up in testing.
Reproduction
This reproduces the line-discipline behavior directly, no Monarch account needed (macOS 26.5.2, CPython 3.13.14):
import os, pty, termios, time
def trial(n):
pid, fd = pty.fork()
if pid == 0:
f = open('/dev/tty', 'r')
attrs = termios.tcgetattr(f)
attrs[3] = attrs[3] & ~termios.ECHO # what getpass does; ICANON stays on
termios.tcsetattr(f, termios.TCSAFLUSH, attrs)
line = f.readline()
os.write(1, b"GOT:%d\n" % len(line))
os._exit(0)
time.sleep(0.4)
os.write(fd, b"C" * n + b"\n")
time.sleep(0.6)
os.set_blocking(fd, False)
try:
out = os.read(fd, 65536)
except Exception as e:
out = repr(e).encode()
os.kill(pid, 9); os.waitpid(pid, 0)
print(f"sent {n:5d} bytes -> {out[:40]!r}")
for n in (900, 1023, 1030, 4000):
trial(n)
Output:
sent 900 bytes -> b'GOT:901\r\n'
sent 1023 bytes -> b'GOT:1024\r\n'
sent 1030 bytes -> b'\x07\x07\x07\x07\x07\x07\x07'
sent 4000 bytes -> b'\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07...'
1023 bytes works, 1030 does not. Past the limit the read never completes, which matches the hang. Interrupting produces:
File "login_setup.py", line 42, in _login_with_cookies
cookie_string = getpass.getpass("Paste the Cookie header value: ").strip()
...
File ".../getpass.py", line 146, in _raw_input
line = input.readline()
KeyboardInterrupt
This is also user-hostile to diagnose, because getpass echoes nothing — there is no feedback distinguishing "waiting for input" from "input silently dropped."
Suggested fixes
Any of these avoids the canonical-mode limit:
- Read the clipboard. The user has just copied the value anyway.
subprocess.run(["pbpaste"], ...) on macOS, xclip -o/wl-paste on Linux, Get-Clipboard on Windows. Best UX by far: no prompt, no paste into a tty at all.
- Read from a file. Prompt for a path, or accept
--cookie-file. Also keeps the secret out of shell history.
- Read from an env var, e.g.
MONARCH_COOKIE.
- Turn off
ICANON for the read and accumulate until newline, instead of relying on the line discipline.
Option 4 keeps the existing UX but is the fiddliest; option 1 or 2 is probably the smallest safe change. A fallback chain (try the tty read, and if it returns empty or the process is on Darwin, fall back to clipboard/file) would preserve current behavior on Linux.
It would also help to validate the parsed result and tell the user what was actually captured — printing cookie names and byte count (never values) makes "I grabbed the wrong request in DevTools" immediately obvious, since the analytics-only cookies from a non-api.monarch.com request are a separate easy mistake.
I worked around it locally with a small clipboard-based script and authenticated successfully on the first try, so the cookie auth path itself works fine — it is only the input method that is broken. Happy to send a PR if you have a preference among the above.
Environment
- macOS 26.5.2 (Darwin 25.5.0), arm64
- Python 3.13.14
- monarchmoneycommunity 1.4.0
- monarch-mcp-server @ ca6c159 (main, cloned 2026-07-26)
Summary
On macOS, option 1 (browser session cookies) in
login_setup.pycan never succeed. The script hangs forever at thePaste the Cookie header value:prompt, and Enter does nothing.The cause is a hard tty limit, not a bad cookie or a slow network call.
Cause
login_setup.py:42reads the cookie withgetpass.getpass():getpassreads a line from/dev/ttyin canonical mode. Canonical-mode input is capped atMAX_CANONbytes per line, and on Darwin that is 1024. A real MonarchCookieheader is several KB (Segmentajs_*, Osano consent, Cloudflare, plus the auth cookies), so the line never terminates: the terminal emits BEL for every byte past the limit and discards it.readline()blocks indefinitely.Linux sets
MAX_CANONto 4096, which is probably why this hasn't shown up in testing.Reproduction
This reproduces the line-discipline behavior directly, no Monarch account needed (macOS 26.5.2, CPython 3.13.14):
Output:
1023 bytes works, 1030 does not. Past the limit the read never completes, which matches the hang. Interrupting produces:
This is also user-hostile to diagnose, because
getpassechoes nothing — there is no feedback distinguishing "waiting for input" from "input silently dropped."Suggested fixes
Any of these avoids the canonical-mode limit:
subprocess.run(["pbpaste"], ...)on macOS,xclip -o/wl-pasteon Linux,Get-Clipboardon Windows. Best UX by far: no prompt, no paste into a tty at all.--cookie-file. Also keeps the secret out of shell history.MONARCH_COOKIE.ICANONfor the read and accumulate until newline, instead of relying on the line discipline.Option 4 keeps the existing UX but is the fiddliest; option 1 or 2 is probably the smallest safe change. A fallback chain (try the tty read, and if it returns empty or the process is on Darwin, fall back to clipboard/file) would preserve current behavior on Linux.
It would also help to validate the parsed result and tell the user what was actually captured — printing cookie names and byte count (never values) makes "I grabbed the wrong request in DevTools" immediately obvious, since the analytics-only cookies from a non-
api.monarch.comrequest are a separate easy mistake.I worked around it locally with a small clipboard-based script and authenticated successfully on the first try, so the cookie auth path itself works fine — it is only the input method that is broken. Happy to send a PR if you have a preference among the above.
Environment