A watchdog for your Claude account's Active Sessions. It checks the session list on a schedule and terminates any session that is not from a location you have protected — while making it very hard to ever terminate one you care about.
It runs on your locally installed Chrome and signs in by reusing your existing claude.ai session cookie, so it does not create a second login or add another device to your session list.
- Protected locations
- Safety rules
- Requirements
- Setup
- Running the bot
- All command-line options
- How sign-in works
- Troubleshooting
- Python version
- Files in this repo
The rule is decided by the country Claude shows at the end of the location:
- Outside India: always kept. Any session whose location ends in a country
other than India (
NO,US,GB, ...) is never terminated. So is any location that shows no country at all. - Inside India (
..., IN): only these cities are kept. Every other Indian city (Pune, Nashik, Latur, Delhi, ...) is terminated.
The Indian cities are hard-coded in index.js and main.py
(PROTECTED_LOCATIONS). The --allow flag can only add cities, never remove
one:
| Location | Matches, for example | |
|---|---|---|
| ✅ | anywhere outside India | Bergen, Vestland, NO, London, England, GB |
| ✅ | Mumbai | Mumbai, Maharashtra, IN |
| ✅ | Navi Mumbai | Navi Mumbai, Maharashtra, IN |
| ✅ | Panvel | Panvel, Maharashtra, IN |
| ✅ | Thane | Thane, Maharashtra, IN |
| ✅ | The current session | the browser the bot itself is using |
| ❌ | any other city in India | Pune, Maharashtra, IN, Nashik, Maharashtra, IN |
India is detected only when the last comma-separated part is exactly IN
or India and a city comes before it, so Indianapolis, Indiana, US is not
mistaken for India. City matching is case-insensitive and substring-based.
To protect an extra Indian city without touching the code:
node index.js --allow "pune,nashik"Beyond the location list, five rules make a wrong termination hard:
- Only the Active sessions table is ever read. The account page also lists
Trusted devices (
Device | Added) just above it. Read blindly, a trusted device'sAddeddate would be parsed as its location, making it look like an unprotected session. The bot locates the table under theActive sessionsheading, maps columns by their header names (so an added column such asUpdatedchanges nothing), and scans nothing else. If that table cannot be identified, the round is skipped entirely. - Unknown location = keep. If the location cell is blank or unreadable (slow render, layout change), the row is kept, never terminated.
- Re-check before acting. The row is re-read from the live DOM immediately before the terminate click. If it now reads differently, or now classifies as protected, the bot skips it and re-scans.
- Runaway guard. If every row looks unprotected — impossible on a healthy account, since the current session is always protected — the bot assumes it misread the page and terminates nothing that round.
- "Log out" can never be clicked. The account page carries a plain
Log outbutton (and on some accountsLog out of all devices), which would end sessions this bot exists to protect. Two independent layers stop it: a capture-phase listener injected into the page swallows any click on a logout control before the app sees it, and every click the bot makes is checked against the element's own label first. Terminating a single session goes through the row menu and itsTerminate sessionconfirmation, neither of which says "log out".
A termination is only counted once the row has actually disappeared from the table.
- Node.js 18+
- Google Chrome installed locally (the bot drives
chrome.exe; it does not need Playwright's bundled Chromium) - A Claude account you are signed in to in that Chrome
git clone git@github.com:Amrit-Nigam/claude-chokidar.git
cd claude-chokidar
npm installThe bot authenticates as you by reusing your existing claude.ai session cookie. To copy it:
- Open https://claude.ai in the Chrome where you are already signed in.
- Press F12 to open DevTools.
- Go to the Application tab.
- In the left sidebar: Storage → Cookies →
https://claude.ai. - Find the row named exactly
sessionKey. - Click it and copy the Value — it starts with
sk-ant-sid02-and is around 130 characters long.
Which cookie exactly? You will see several similar names. Use this mapping:
| Cookie name in Chrome | .env key |
Needed? |
|---|---|---|
sessionKey |
SESSION_KEY |
Yes — this is the one |
sessionKeyV3 |
SESSION_KEYV3 |
Optional |
sessionKeyLC |
SESSION_KEYLC |
Optional |
sessionKeyV3LC |
SESSION_KEYV3LC |
Optional |
SESSION_KEY on its own is enough — that is the configuration this project is
tested with. Add the optional ones only if sign-in ever fails without them.
If the cookie list shows the same name twice (claude.ai keeps one host-only
and one domain-wide copy), put the second value in a _v2 key, for example
SESSION_KEYV3_v2.
Copy the Value only — not the cookie name, not quotes, not a trailing semicolon.
cp .env.example .envThen edit .env:
SESSION_KEY=sk-ant-sid02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.env is gitignored. Treat it like a password — anyone holding that value can
use your Claude account.
On Windows, do not create it with
echo ... > .envin PowerShell. PowerShell writes UTF-16, which corrupts the value. Use an editor, or run:npm run session-key -- "sk-ant-sid02-..."which writes a correctly encoded
session-key.txtinstead.
Run one scan in preview mode. Nothing is ever terminated in --dry-run:
npm run dry-runExpected output:
Protected (always): outside India, and in India: mumbai, navi mumbai, panvel, thane
Mode : DRY-RUN (Preview only)
Using your local Chrome: C:\Program Files\Google\Chrome\Application\chrome.exe
Reusing your existing claude.ai session (sessionKey) - no new sign-in.
Safety guard armed: every "Log out" control is blocked in this window.
[17:13:55] Check #1: Inspecting active sessions...
[KEEP] Chrome (Mac OS X) | Location: Mumbai, Maharashtra, IN | Protected location
[KEEP] Chrome (Windows) Current | Location: Mumbai, Maharashtra, IN | Current session
[KEEP] Claude (iOS) | Location: Bergen, Vestland, NO | Outside India (or country not shown)
[WOULD TERMINATE] Chrome (Linux) | Location: Pune, Maharashtra, IN | Created: ...
[17:13:55] Check complete. Total: 4 | Kept: 3 | Flagged: 1
Check that the [KEEP] lines list everything you expect to survive, then run it
for real.
npm startThat starts the watchdog and scans every 60 seconds until you stop it with
Ctrl + C.
Other one-off modes:
npm run once # scan once, terminate, exit
npm run dry-run # scan once, terminate nothing, exit
npm run bot:dry-run # keep scanning, terminate nothing (monitoring mode)Pick a ready-made interval:
| Interval | Command | Headless (no window) |
|---|---|---|
| Every 30 seconds | npm run bot:30s |
npm run bot:30s:headless |
| Every 1 minute (default) | npm run bot:1m or npm start |
npm run bot:1m:headless |
| Every 2 minutes | npm run bot:2m |
npm run bot:2m:headless |
| Every 3 minutes | npm run bot:3m |
npm run bot:3m:headless |
| Every 4 minutes | npm run bot:4m |
npm run bot:4m:headless |
| Every 5 minutes | npm run bot:5m |
npm run bot:5m:headless |
| Every N minutes (1–60) | npm run bot:<N>m |
npm run bot:<N>m:headless |
Every whole minute from bot:1m to bot:60m exists, each with a :headless
variant. Any other flag can be passed after --, e.g.
npm run bot:7m -- --dry-run.
Or set any interval you like, in seconds or with a unit (s, m, h):
node index.js --interval 120 # every 2 minutes
node index.js --interval 90m # every 90 minutes
node index.js --interval 2h --headless
node index.js --interval 3600 # every hourBetween scans the bot prints when the next check is due:
⏳ Next check in 300s (Press Ctrl+C to stop)...
A shorter interval reacts faster; a longer one is gentler on the account and your machine. Every 5 minutes is a good middle ground if 1 minute feels noisy.
Once your .env works, add --headless so no window is shown.
Windows (PowerShell)
# Start detached, logging to bot.log
Start-Process -WindowStyle Hidden -FilePath node `
-ArgumentList "index.js","--interval","300","--headless" `
-RedirectStandardOutput bot.log -RedirectStandardError bot.err
# Watch the log
Get-Content bot.log -WaitTo start it automatically at login, create a Task Scheduler task that runs
node D:\path\to\claude-chokidar\index.js --interval 300 --headless.
macOS / Linux
nohup node index.js --interval 300 --headless > bot.log 2>&1 &
tail -f bot.log
pkill -f "node index.js"With PM2 (any OS)
npm install -g pm2
pm2 start index.js --name claude-chokidar -- --interval 300 --headless
pm2 logs claude-chokidar
pm2 save && pm2 startup # survive rebootsOn a Linux server, run the bot under notify-wrapper.js via systemd instead
of pm2. The wrapper runs npm run bot:1m:headless, and when something goes
wrong it pauses the bot and emails you:
| Problem | What the wrapper does |
|---|---|
| Session key expired / rejected | retries once, then pauses + emails |
| Logged out mid-run ("Log out of all devices", revoked key) | retries once, then pauses + emails |
| Crash loop (4 failures in 30 min) | pauses + emails |
| Hung (no scan for 10 min) or 5 unreadable scans in a row | restarts; counts toward the crash loop |
| systemd gives up restarting the wrapper | claude-chokidar-alert.service emails |
While paused, a reminder goes out every 24h. Saving a new key
(npm run session-key -- <value>) resumes it automatically within ~15s and
sends a "back online" email; npm run chokidar:resume clears any other pause.
Setup (Ubuntu, Node 20, Google Chrome installed):
cd ~/claude-chokidar && npm ci --omit=dev
# .env: SMTP_USER / SMTP_PASS (Gmail app password), NOTIFY_EMAIL, NOTIFY_ALSO - see .env.example
npm run session-key -- "<sessionKey value>"
npm run notify:test # confirm the email arrives
sudo cp deploy/*.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now claude-chokidar
journalctl -u claude-chokidar -fUsage: node index.js [options]
--interval, -i <time> Time between scans: seconds, or 90s / 7m / 2h
(default: 60)
--once Run a single scan and exit
--dry-run Preview only; never terminates anything
--allow, -a <list> Extra protected Indian cities, comma-separated.
ADDED to the always-protected list; the built-in
protected locations cannot be disabled.
--session-key <value> Session cookie to use (overrides .env)
--chrome-profile <name> Local Chrome profile: folder, display name or email
--chrome-path <exe> Path to chrome.exe (default: auto-detected)
--separate-profile Never touch the real Chrome profile
--cdp [port] Attach to a Chrome already running with a debug port
--user-data-dir <dir> Where to persist the bot's browser data
(default: ./chrome_session)
--headless Run without a visible window
--list-profiles List local Chrome profiles and exit
-h, --help Show help
Examples:
# Every 15 minutes, headless, also protecting Pune
node index.js --interval 900 --headless --allow "pune"
# Use a different Chrome profile
node index.js --chrome-profile you@example.com
# See which profiles exist
npm run profilesThe bot reads your session cookies in this order of precedence:
--session-key "<value>"on the command line- the
CLAUDE_SESSION_KEYenvironment variable session-key.txt(written bynpm run session-key -- "<value>").env← the normal setup
If none is configured, the bot opens a window and waits for you to sign in once;
that login is then remembered in ./chrome_session. Note that signing in this
way does add one extra session to your list.
Why it cannot just read your everyday Chrome profile. Chrome blocks this by design, and both doors are shut:
- Chrome 136+ refuses remote debugging when running on the default user-data-dir, so Playwright cannot drive your normal Chrome as-is.
- Chrome's app-bound cookie encryption (
v20cookies) means a copied profile decrypts to nothing — the copy is always signed out.
Reusing the cookie is the supported way through. If you would rather attach to a
real browser, close Chrome completely and run npm run chrome:debug, then
npm run connect.
"This browser profile is not signed in to Claude"
Your SESSION_KEY is expired, truncated, or was written as UTF-16. Re-copy the
sessionKey value from DevTools and update .env. Session cookies do expire —
expect to refresh this occasionally.
Sign-in fails even with a fresh SESSION_KEY
Add the optional SESSION_KEYV3 (and SESSION_KEYV3_v2 if the name appears
twice) as described in step 2.
"Could not find 'Active sessions' table" The page did not finish rendering, or you are not signed in. The bot skips that round and retries on the next interval; nothing is terminated.
"Chrome is already running, so it cannot be switched into debug mode"
Informational. The bot uses its own profile directory (./chrome_session) and
carries on — your everyday Chrome is untouched and stays usable.
Nothing gets terminated
Check the [KEEP] reason printed for each row. Location unknown - kept for safety means the location cell did not render; Protected location means it
matched your protected list.
A session you want removed is in a protected city That is deliberate: protected locations cannot be terminated by this bot. Remove it from claude.ai's settings page yourself.
main.py mirrors index.js — same protected locations, same safety rules, same
.env handling.
pip install -r requirements.txt
playwright install chromium
python main.py --dry-run --once # preview
python main.py --interval 300 # every 5 minutesThe Node version is the one that is actively exercised; the Python port is kept in step but is not equally tested.
| File | Purpose |
|---|---|
index.js |
The bot: scanning, classification, termination, safety guards |
main.py |
Python port of the same bot |
chromeProfile.js |
Finds your local chrome.exe and Chrome profiles |
save-session-key.js |
Writes session-key.txt with correct encoding |
launch-chrome-debug.js |
Starts your own Chrome with a debug port |
notify-wrapper.js |
Always-on supervisor: emails and pauses on expiry, logout, crashes |
deploy/*.service |
systemd units for the supervisor and its failure alert |
.env.example |
Template for your .env |
chrome_session/ |
The bot's browser profile (gitignored) |
.env, session-key.txt and chrome_session/ are all gitignored and must stay
out of version control.