Your Cron Job Exits 0 and Does Nothing: Reading Chrome's Cookie SQLite to Know If a Session Is Actually Alive A developer discovered that an Instagram automation job was silently failing for days because the session had expired but the job still returned exit 0. The fix involved reading Chrome's cookie SQLite database to check for the presence of the 'sessionid' cookie, which directly indicates login state, rather than relying on DOM structure. The developer built a script called 'profile-session-guard.sh' to proactively verify session liveness and stop the lane if expired. An automation job can fail for days without making a single sound. Mine did: one Instagram lane ran twelve times a day, logged "done" every time, raised zero errors — and liked exactly nothing. This post is about that failure mode and how I fixed the detection, in a series where I share the holes I've actually fallen into while mass-producing personal projects. I went from ¥100k/month as a university student, to ¥600k/month juggling side gigs, to zero income after being laid off, and then rebuilt an autonomous environment with Claude Code — now at ¥1.2M/month in revenue. This time, the story is: "the job ran every day, produced zero results, and nobody noticed." The scary thing about automation is that when it breaks, it makes no noise. The job for my Instagram auto-like system social-autolike was working normally until the morning of 2026-08-09 — or at least it looked that way. The lane tied to the account ig-2 was scheduled to run 12 times a day. Each run logged "complete." No errors reached the monitoring dashboard. The reality: likes = 0, follows = 0. It had been doing absolutely nothing for days. The cause was simple. The ig-2 account was logged out. Its Instagram session had expired. But the job, staring at a logout screen, kept returning exit 0 on the grounds that "there were no targets left" or "no accounts matched the criteria." The job did have login-detection code at the time. needLogin in social-autolike/src/run.js decided that login was required based on roughly these two conditions: /login article / role=feed / video / main / data-e2e exist The problem is the second half of that second condition. Instagram's logout screen has a main element. It's still there today — an element placed as part of the page's semantic structure. Because the DOM check was built on the idea that "zero main elements means login is needed," opening the logout screen still found one main, and the check slipped through. The selector measures "does this look like a login screen?" But what I actually want to know is the authentication state itself: "is this browser currently logged in?" Those are two different things, and the former slips through the moment the UI's structure changes. Chrome's session management is recorded in a SQLite database file at Default/Network/Cookies or Default/Cookies , depending on the Chrome version . Instagram's login state is managed by a cookie named sessionid , and if that cookie exists for the instagram.com domain, that profile is logged in. The decisive difference from the DOM is that this is not the appearance of the UI — it is the fact of authentication. No matter how much Instagram redesigns things, no matter what elements they add to the logout screen, the presence of the sessionid cookie maps one-to-one to login state. Making cookies your primary source is a shift from guessing to observing the fact. Playwright, Puppeteer, browser-use, or something you wired together with Claude Code — whatever the shape, any automation that accesses a web service using a Chrome or Chromium profile can have this exact problem. When a job returns "0 results, exit 0," how do you currently distinguish "there genuinely were no targets" from "it was logged out and couldn't see anything"? Even if the number on your monitoring graph is pinned at zero, it's hard to notice as long as no errors fire. By the time you do notice, days' worth of results are already gone. In my case, I couldn't even determine exactly how many days ig-2 had lost. The profile-session-guard.sh I built this time is a script for answering that question. It reads Chrome's cookie DB directly to confirm up front whether the session is alive, and if it's expired, it notifies and stops the lane. Below is the full picture. What the script does breaks into four stages. 設定ファイル3本 accounts.json ig-reply-accounts.json ─→ プロファイルdir一覧を解決 post-accounts.json + 重複を1件に集約 + 固定2件(別リポ) ↓ 各プロファイルdirでCookie DBを探す Default/Network/Cookies Default/Cookies ─→ 見つかったら mktemp でコピー Network/Cookies Cookies ↓ sqlite3 でCookieを検索 platform別のSQLクエリ ─→ OK / EXPIRED / UNKNOWN を判定 コピーは終了時に削除 ↓ 結果を集計してstateファイルと照合 切れ方が前回と同じ → 通知スキップ 新しい切れ → Discord通知 + exit 1 全員OK → exit 0 Translation of the diagram: three config files → resolve the list of profile dirs, collapsing duplicates; then look for the cookie DB in each profile dir and copy it with mktemp when found; then query cookies with sqlite3 using a per-platform SQL query to decide OK / EXPIRED / UNKNOWN, deleting the copy on exit; then aggregate the results and compare against the state file — same breakage as last time → skip the notification, new breakage → Discord notification + exit 1 , everyone OK → exit 0 . Let's walk through it in order. The script's first job is to determine which profiles to inspect. The earlier implementation hardcoded the list of profile dirs inside the script. As a result, the very next day it falsely reported metrics-hub/profiles/tiktok and tiktok2 — old dirs not used by any actual job — as "logged out." The real TikTok lanes were using different dirs via the reuseProfile field in social-autolike/accounts.json . A hardcoded list starts rotting the moment you write it.The current implementation resolves them from the config files the jobs actually reference. load accounts { local lane platform dir if "$JQ" -r --arg root "$ROOT" \ '.accounts | .id, .platform, .reuseProfile // $root + "/profiles/" + .id | @tsv' \ "$ACCOUNTS CONFIG" "$SOURCE FILE"; then log "unknown resolver: accounts.json could not be read" return 1 fi while IFS=$'\t' read -r lane platform dir; do add candidate "$dir" "$platform" "$lane" done <"$SOURCE FILE" } The key is the .reuseProfile // $root + "/profiles/" + .id part of the jq query. If reuseProfile is written in the config file, use that path; otherwise fall back to the default path profiles/