How My Own Script Killed Instagram Sessions Every Night: pkill and a Shared Chrome Profile A developer's own automation script was killing Instagram login sessions nightly, disrupting a revenue-generating account. The issue stemmed from two separate repositories sharing the same Chrome profile, where one script's health check force-killed Chrome via pkill, destroying the session cookies. The developer added monitoring to detect zero-output runs and total like counts to address the problem. I built a fleet of 171 automation jobs to grow my business — and one of them was quietly murdering another one's login session every single night for days before I figured out why. Here's the arc: I made ¥100k/month as a university student, stacked side jobs up to ¥600k/month, got laid off and went back to zero, then spent six months building an autonomous Claude Code environment that now does ¥1.2M/month in revenue. The Instagram account underpinning that revenue was being killed daily by a script I wrote myself . When you're building up revenue as a solo developer, the first wall you hit isn't "not enough hands" — it's " I have no idea what's happening while I sleep ." In my current environment, 171 jobs launch automatically from launchd every day. Posting, liking, following, unfollowing, and DMs across X / Instagram / Threads / TikTok are all automated, and a Bash script called sns-output-watchdog.sh monitors whether each one actually produced output that day, based on artifact logs. But before I built that monitoring, I have a track record of not noticing for two weeks . The header comment of sns-output-watchdog.sh says this: content-watchdog.sh は article/note/maker/series/ameba だけを見ており、 X/IG/TikTok/Threads の投稿・返信は誰も監視していなかった。その結果 IG投稿は7/29から、TikTok投稿は7/28から止まったまま2週間気づかれなかった。 If IG posting stops, new followers stop coming in, the flow into my official LINE account stops, and revenue growth eventually flattens. A single failed slot doesn't mean "I missed one post today" — it means "the entire funnel that was supposed to start there was dead." Even after I set up monitoring, the next problem showed up: exit 0 and "something actually happened" are different things . In measurements on 2026-08-08, the IG like lane ig-1 finished with exit 0 and zero likes on 8 out of 12 daily runs. The watchdog at the time only counted "how many times the marker 終了 appeared," so it judged both circuit-break and need-login as "healthy." 🔴 従来は '終了 ' の出現回数だけを数えていたため、circuit-break でも need-login でも 「健全」と判定していた。2026-08-08 実測で ig-1 は12run中8回、tt-1/tt-2 は6割が いいね0件のまま exit 0 で終わっており、2週間誰も気づけなかった。 Learning from that, I added count dead runs to detect runs with zero output, and sum likes today to total the actual like count. But that still didn't solve the structural problem: the side doing the breaking and the side reporting the breakage lived in different repositories . That's the 2026-08-24 incident. If you run multiple automations on the same Mac, you're at risk of falling into exactly the same hole. To lay out the problem, here's the structure as a diagram. scent-media リポジトリ social-autolike リポジトリ ────────────────────────── ────────────────────────────────── scripts/ensure chrome.sh config/accounts.json └ CDP :9223 へ接続試行 └ "reuseProfile": ".profiles/chrome-ig" 応答なし → pkill 実行 │ th-scent ジョブ(Playwright) │ └ .profiles/chrome-ig を掴んで起動 │ ※ CDP 9223 は一切開かない ↓ ↓ └──────────────┬───────────────────────────┘ ↓ .profiles/chrome-ig ← 両者が同じプロファイルを参照 Cookie SQLite 強制破壊 instagram.com の sessionid → 0行 ensure chrome.sh in scent-media checks, before an IG post, whether Chrome is in a CDP-controllable state. The check is an attempted connection to port 9223. If there's no response, it decides "Chrome is dead" and force-kills it with this command: pkill -f "user-data-dir=$PROFILE" $PROFILE is .profiles/chrome-ig . The problem is that the th-scent job in social-autolike shared that same profile via a reuseProfile setting. th-scent launches Chrome through Playwright, but it never opens CDP port 9223. From ensure chrome.sh 's point of view, "the Chrome that th-scent is using" always looks like "9223 is closed = a dead process."This collision wasn't random — the launchd schedule made it happen deterministically at fixed times every day . | Time | Job | Duration | |---|---|---| | 5:18 / 13:18 / 21:18 | th-scent autolike | up to 40 min | | 6:56 / 14:56 / 22:56 | th-scent unfollow | up to 40 min | | 7:20 | scent-media daily-generate | — | | 19:00 / 21:00 / 23:00 | scent-media daily-post IG carousel post | — | At 22:56, th-scent unfollow grabs .profiles/chrome-ig ; at 23:00, daily-post calls ensure chrome.sh . 9223 doesn't respond, so pkill runs. This repeated every night. The 19:00 and 21:00 slots overlap with th-scent autolike in the same way. pkill -f "user-data-dir=..." sends SIGTERM and then SIGKILL. Chrome can't complete its shutdown routine and loses the chance to write the profile's Cookie SQLite back in a consistent state. On next launch, Chrome recreates an empty Cookie DB . Here are the measured values: The cookies table shrank to 3 rows total across all hosts, and instagram.com had 0 cookies. The IG sessionid was gone. On 2026-08-24, all three slots — 19:00, 21:00, and 23:00 — posted nothing. The IG post check in sns-output-watchdog.sh looks like this: check "ig-autopost" \ "$ count today "$CL LOGS/sns-ig-autopost.retry.log" "$TODAY JST" 'OK posted' " \ 1 "IGカルーセル投稿" \ "$CL LOGS/sns-ig-autopost.retry.log" "$TODAY JST" If OK posted doesn't appear even once in the day's log, the lane goes into FAILED and 🚨 SNS当日未出力: ig-autopost fires to Discord. That alert was firing. But the text of the alert says "posting failed," not "session destroyed by pkill." The session checker correctly reports "there is no IG sessionid." need-login shows up in the log. A human reads that and concludes "the login expired → let's log in again." The next day, at the same time, pkill runs again and the cookies vanish. This loop kept repeating. watchdog: 🚨 ig-autopost 未出力 ↓ session-liveness: sessionid が無い(正しい報告) ↓ 人間: GUI 再ログインを実行 ↓ 翌日 22:56: th-scent がプロファイルを掴む ↓ 翌日 23:00: ensure chrome.sh → pkill → Cookie 消滅 ↓ watchdog: 🚨 ig-autopost 未出力(同じ報告が出る) Because the side doing the breaking ensure chrome.sh / scent-media and the side reporting the breakage retry.log / the session checker live in different repositories , reading only one of them will never connect the dots. The reuseProfile setting written in social-autolike 's config/accounts.json appears nowhere in scent-media 's code.The fix comes down to one thing: eliminate pkill entirely and replace it with waiting and skipping . 修正後の ensure chrome.sh(概要) if pgrep -f "user-data-dir=$PROFILE" /dev/null; then 他プロセスが掴んでいる → 10秒間隔・最大420秒ポーリング for i in $ seq 1 42 ; do sleep 10 pgrep -f "user-data-dir=$PROFILE" /dev/null || break done if pgrep -f "user-data-dir=$PROFILE" /dev/null; then echo "他ジョブが使用中 pid=$ pgrep -f "user-data-dir=$PROFILE" 。Cookie破壊を避けるため起動を見送る" &2 exit 2 「今スロット見送り」の専用コード fi fi ここまで来たら誰も掴んでいない → SingletonLock等の掃除 → 起動処理へ The caller, daily post.sh , receives exit 2 in a separate branch and treats it as "skip this slot, retry at the next one," exiting with exit 0 . For verification I ran two bash -n syntax checks, confirmed grep -c pkill returned 0, and confirmed the exit 2 path really exists at line 57, then bundled it into commit 15307d7 . Why I didn't fold exit 2 into "error," what the "3 slots per day" premise behind the skip design means, and how I reworked the watchdog so it won't have the same structural blind spot again — I break all of that down in the next part. count today and has today The first thing you agonize over when writing a watchdog is distinguishing "zero count = failure" from "it just didn't run." In sns-output-watchdog.sh I handle this with two independent functions. 当日分のログ行だけに成功マーカーがあるか数える。 count today { local file="$1" daymark="$2" marker="$3" -f "$file" || { echo 0; return; } /usr/bin/awk -v day="$daymark" -v mark="$marker" ' index $0, day { seen = 1 } seen && index $0, mark { n++ } END { print n + 0 } ' "$file" } そのログに「当日を示す行」自体があるか。 has today { local file="$1" daymark="$2" -f "$file" || return 1 /usr/bin/grep -qF "$daymark" "$file" } count today uses awk's seen flag so that "only lines after the day marker appears" are considered. A simple grep -c marker would mix in success lines from previous days. The logs are designed not to rotate, so if this one-day offset breaks, you get "today judged healthy based on yesterday's post count." has today matters because of the branching inside check . check { local lane="$1" count="$2" min="$3" note="$4" file="${5:-}" daymark="${6:-}" ... if "$count" -ge "$min" ; then log "ok lane=$lane count=$count" return fi if -n "$file" && has today "$file" "$daymark"; then UNKNOWN="${UNKNOWN:+$UNKNOWN,}$lane" log "UNKNOWN lane=$lane 当日行なし" return fi FAILED="${FAILED:+$FAILED,}$lane" } When the count falls below min, if has today returns false the lane goes into UNKNOWN rather than FAILED. If you conflate the two, a lane that only runs three times a week will emit UNHEALTHY every day — Monday, Wednesday, and Friday included. The moment alerts stop being trusted, your monitoring is finished. count dead runs — Catching Runs That Finished but Produced Nothing count dead runs { local file="$1" day="$2" -f "$file" || { echo ""; return; } /usr/bin/grep -a "^$day" "$file" 2 /dev/null \ | /usr/bin/grep -E '終了 \ need-login|circuit-break|error|rate-limit \ ' \ | /usr/bin/grep -c 'いいね:0' || true } There's a reason this is a two-stage grep. The first regex, 終了 \ need-login|circuit-break|error|rate-limit \ , narrows to "lines with a harmful termination reason," and the second, grep -c 'いいね:0' , narrows to "and zero output." If you counted a run that terminated early on rate-limit but still managed a few likes as a "dead run," you'd get an alert every time a mild nighttime rate limit kicks in. The point is to AND the two conditions to isolate "genuinely accomplished nothing." This function is used in the outcome-based monitoring loop. for lane in x-1 ig-1 ig-2 ig-3 ig-sug th-1 th-2 tt-1 tt-2 tt-3; do lf="$SA LOGS/$lane.log" -f "$lf" || continue likes=$ sum likes today "$lf" "$TODAY UTC" dead=$ count dead runs "$lf" "$TODAY UTC" if -n "$likes" && "$likes" = "0" ; then FAILED="${FAILED:+$FAILED,}$lane-likes0" log "UNHEALTHY lane=$lane 本日のいいね合計=0 成果ゼロrun=${dead:-?}回 " elif -n "$dead" && "${dead:-0}" -ge 3 ; then FAILED="${FAILED:+$FAILED,}$lane-dead${dead}" log "UNHEALTHY lane=$lane 成果ゼロrunが${dead}回 いいね合計=${likes} " else log "OK lane=$lane いいね合計=${likes:-?} 成果ゼロrun=${dead:-0}回" fi done The branch differs between likes being an empty string the file itself doesn't exist and being "0" . In the empty case it simply skips to the next lane rather than reporting UNKNOWN. That subtle distinction is what keeps "a lane not yet promoted to monitored status" from being confused with "a monitored lane that isn't running." sum likes today sum likes today { local file="$1" day="$2" -f "$file" || { echo ""; return; } /usr/bin/grep -a "^$day" "$file" 2 /dev/null \ | /usr/bin/grep -oE 'いいね: 0-9 +' \ | /usr/bin/grep -oE ' 0-9 +' \ | /usr/bin/awk '{s+=$1} END {print s+0}' } Log lines start with the 2026-08-24 ... format, so stage one narrows to today's lines with "^$day" . Stage two extracts marker-prefixed numbers with grep -oE 'いいね: 0-9 +' , and stage three strips down to the bare digits and sums them with awk. The reason for the intermediate grep is that log lines can contain multiple markers, like いいね:0 フォロー:3 — pulling with just 0-9 + would also pick up the 3 from フォロー:3 . The フォロー total uses the same structure in a separate function, sum follows today . Likes and follows are independent outcomes, and there are cases where likes are zero but follows are working, so they must always be judged separately. Here's the core of the fix in real code. Before the fix, it tried to solve everything in one line. 修正前:CDP 9223 が応答しない → 無条件で殺す pkill -f "user-data-dir=$PROFILE" After the fix, it's three stages. 修正後 if pgrep -f "user-data-dir=$PROFILE" /dev/null; then 他プロセスが掴んでいる → 10秒間隔・最大420秒ポーリング for i in $ seq 1 42 ; do sleep 10 pgrep -f "user-data-dir=$PROFILE" /dev/null || break done if pgrep -f "user-data-dir=$PROFILE" /dev/null; then echo "他ジョブが使用中 pid=$ pgrep -f "user-data-dir=$PROFILE" 。Cookie破壊を避けるため起動を見送る" &2 exit 2 fi fi ここまで来たら誰も掴んでいない → SingletonLock 等の掃除 → 起動処理へ The reason I didn't fold exit 2 into "error" exit 1 rests on a design fact. IG carousel posting has three slots a day: 19:00, 21:00, and 23:00. Skipping one slot still leaves the next slot to post the same content. With exit 1 , on the other hand, Discord alerts would keep firing every time the th-scent collision window comes around, and genuinely abnormal alerts would drown in the noise. "Skipped" is a third state that is neither failure nor success , and representing it with a dedicated exit code lets the caller, daily post.sh , receive it in an independent branch. It also matters that the SingletonLock cleanup now happens only when nobody is confirmed to be holding the profile . Before the fix, it unconditionally ran rm -f SingletonLock SingletonSocket SingletonCookie right after pkill. If you delete lock files while someone is using the profile, the process using it crashes and leaves the profile in a half-broken state. My first watchdog implementation pointed the IG post check at the wrong file. 🔴 誤り:err.log は 2026-08-09 で更新が止まっている check "ig-autopost" \ "$ count today "$CL LOGS/sns-ig-autopost.err.log" "$TODAY JST" 'OK posted' " ... In reality, post success/failure logs go to retry.log , and err.log had stopped updating after 2026-08-09. has today kept returning false, and every morning ⚠️ SNS監視が判定不能: ig-autopost(当日行がログに無い) arrived in Discord. Judged by symptoms alone, it looks like "the ig-autopost log is broken." The log wasn't actually broken — I just had the filename wrong. It took me two days to find the cause. I only noticed after checking the file's mtime with ls -la . The comment is still in the code: 成果は err.log ではなく retry.log に出る。err.log は 2026-08-09 で更新が止まっており、 ここを見ている限り毎日 UNKNOWN 誤報になる 実際は当日3本投稿できていた 。 check "ig-autopost" \ "$ count today "$CL LOGS/sns-ig-autopost.retry.log" "$TODAY JST" 'OK posted' " ... A monitoring script that reads log files can't notice "the file went stale = monitoring is dead" unless there's a mechanism that periodically checks the mtime of the files it reads . Receiving false alarms for two weeks is the same state as receiving no alerts at all. TikTok's log inherited the format launchd emits, so its date format differs from the other lanes. X・IG・Threads: TODAY UTC = "2026-08-24" TikTok だけ: TODAY HUMAN = "Mon Aug 24" (%e で日を空白詰め → "Mon Aug 8") TODAY HUMAN="$ date '+%a %b %e' " %e pads the day with a space, so August 8th becomes Aug 8 two spaces . Grepping TikTok's log with "^$TODAY UTC" never matched the date format at all, so it always returned 0. tt ok=$ count today "$CL LOGS/tiktok-bokuwalily.log" "$TODAY HUMAN" 'run end exit 0 ' tt maybe=$ count today "$CL LOGS/tiktok-bokuwalily.log" "$TODAY HUMAN" 'run end exit 2 ' check "tt-autopost" "$ tt ok + tt maybe " 1 \ "TikTok投稿 exit0=$tt ok exit2=$tt maybe " "$CL LOGS/tiktok-bokuwalily.log" "$TODAY HUMAN" The fix is just splitting out a separate variable that uses TODAY HUMAN , but the reason I got stuck finding the cause is that "since every other lane was working fine, the TikTok problem looked like a TikTok-side defect." Format inconsistencies inside a script are invisible unless you line them up against the working lanes and compare. count undecidable today , which I added on 2026-08-22, was born from this lesson. count undecidable today { local file="$1" day="$2" -f "$file" || { echo "0 0"; return; } /usr/bin/awk -v day="$day" ' index $0, day && index $0, "follow成功" { success++ } index $0, day && index $0, "follow判定不能" { undecidable++ } END { print success + 0, undecidable + 0 } ' "$file" } The friendship API is Instagram's private API, used to check follow relationships. When it gets throttled, "is this person following me back?" becomes undecidable, and the job records follow判定不能 and moves on. Likes are working fine, so the sum likes today check passes. From the outside, "ig-1 is healthy again today." The actual symptom was 2255 uncollected entries piled up in ff-ig-2 . Since unfollows can't happen, the backlog grows, you approach the follow limit, and one day follows suddenly stop too. I added the combination of follow成功=0 AND follow判定不能 =20 as a dedicated detection condition. for lane in ff-ig-1 ff-ig-2 ff-ig-3; do ... if "$follow success" -eq 0 && "$undecidable" -ge 20 ; then FAILED="${FAILED:+$FAILED,}$lane-blocked" log "UNHEALTHY lane=$lane follow成功=0 判定不能=$undecidable - friendship APIが絞られている疑い" fi done The source-followers lane is a subprocess that collects follower lists to find follow targets. Until 2026-08-22 it wasn't included in the monitoring loop at all. 2026-08-22 追加前は、このレーンの成果は一度も監視されていなかった。 実害: ig-sug が 23:14/02:13/11:16/14:16 と4回連続で 「coverage 0/17 - 全ソース取得失敗 - exit 1」になり日次20件で止まっていたのに、 誰にも報告されなかった(同時刻に ig-3 も14時間ゼロ成果)。 実体は private API の 429。 ig-sug emitted 全ソース取得失敗 in four slots: 11 PM, 2 AM, 11 AM, and 2 PM. It was falling over with exit 1 at zero coverage against 17 target accounts, but because sum likes today on the main lane's ig-sug.log was working normally on its own, the watchdog summary said OK . The monitoring I added watches source-followers- .log in an independent loop. for lane in ig-1 ig-2 ig-3 ig-nagi ig-sug th-1 th-2 th-3 th-nagi x-1 x-2 x-nagi x-reina; do sf="$SA LOGS/source-followers-$lane.log" -f "$sf" || continue sf dead=$ count today "$sf" "$TODAY UTC" '全ソース取得失敗' if "$sf dead" -ge 2 ; then FAILED="${FAILED:+$FAILED,}$lane-srcfollow-blocked" log "UNHEALTHY lane=$lane-srcfollow 全ソース取得失敗=${sf dead}回 - APIスロットリング疑い" fi done I use sf dead = 2 as the threshold because a one-off failure a temporary 429 self-recovers on the next run. Two consecutive failures let you conclude "the throttling is ongoing." This was the nastiest failure of all. The symptoms were clear. No OK posted at all in IG's sns-ig-autopost.retry.log . 🚨 SNS当日未出力: ig-autopost arriving in Discord from the watchdog. The session checker correctly reporting sessionid がない(need-login) . I re-logged in via the GUI every time. It would be back the next morning. It would be gone again the next night. This cycle went on for several days, and I was starting to form the hypothesis that "maybe Instagram shortened its session lifetime." In reality, ensure chrome.sh was pkill-ing the profile used by social-autolike 's th-scent job every night. th-scent launches Chrome with Playwright but never opens CDP port 9223. From ensure chrome.sh 's point of view, "9223 is closed = a dead process." pkill sends SIGTERM → SIGKILL, Chrome can't complete its shutdown routine, and it can't write the Cookie SQLite back in a consistent state. On the next launch, Chrome recreates an empty Cookie DB. I found the cause when I lined up the log timestamps side by side. 22:56 th-scent unfollow start .profiles/chrome-ig を掴む 23:00 daily-post → ensure chrome.sh → CDP 9223 応答なし 23:00 pkill -f user-data-dir=.profiles/chrome-ig 23:00 Cookie SQLite 破壊 → 空の DB 作り直し 23:00 daily-post: need-login → 投稿ゼロ Because "the side doing the breaking ensure chrome.sh / scent-media " and "the side reporting the breakage retry.log / the session checker " live in different repositories, reading only one of them will never connect the dots. The setting "reuseProfile": ".profiles/chrome-ig" in social-autolike 's config/accounts.json appears nowhere in scent-media 's code. You must not write a presence check for a shared resource as "does the interface I expect respond?" — this incident was the first time I learned that lesson. 9223 being closed doesn't mean "dead"; it may mean "not launched in my particular style." Identify the owner with interface-independent means pgrep / lock files , and allow kill only against processes you started yourself — that principle is what led to the pkill removal in commit 15307d7 . Here are the traps I hit in an environment running multiple repositories on the same Mac. On top of the "wrong err.log filename," "TikTok date format difference," "friendship API throttling," "source-follow monitoring gap," and "Cookie destruction via pkill" detailed in the previous part, the same structural hole showed up in other forms too. Unfollow monitoring didn't exist at all. Unfollow lanes like ig-1.unfollow.log / ig-2.unfollow.log / tt-1.unfollow.log weren't in the monitoring loop at all until 2026-08-17. The damage shows up in measured numbers: ig-1 had 712 unfollows uncollected for over 72 hours, ig-2 piled up to 2255, and tt-1 was unable to unfollow 11 times in a row — while the watchdog returned result=healthy every day. When unfollows jam up, you approach the follow limit, and a few days later follows stop too. Because the direct symptom follows stopped is one step removed from the root cause unfollows jammed first , digging out the cause takes time. "Zero like attempts" is a different failure from "zero like results." th-2 was launching daily with likeBudget=400 , but the log contained not a single line with the string live like . It was only attempting follows and ending on circuit-break . From sum likes today 's point of view, this looks like one verdict: "total likes = 0." But "attempted and got blocked every time" and "never attempted at all" have completely different causes. The former is an account restriction; the latter is a selector mismatch or an action-flow bug. The detection loop that makes this distinction possible now lives at lines 244–259 of sns-output-watchdog.sh : 「起動した」記録があるのに「いいね試行」が0回 = セレクタ不一致/アクション制限の疑い for lane in x-1 x-4 x-5 ig-1 ig-2 ig-3 ig-sug th-1 th-2 tt-1 tt-2 tt-3; do lf="$SA LOGS/$lane.log" -f "$lf" || continue started=$ count today "$lf" "$TODAY UTC" '開始 ' attempts=$ count today "$lf" "$TODAY UTC" 'live like' if "$started" -ge 1 && "$attempts" -eq 0 ; then FAILED="${FAILED:+$FAILED,}$lane-noattempt" log "UNHEALTHY lane=$lane いいね試行が0回 セレクタ不一致/アクション制限の疑い 起動=${started}回" fi done Mixing FAILED and UNKNOWN destroys trust in your alerts. If you run a lane that only launches three times a week through check every day, the days it doesn't launch have no log lines for that day, so it's treated as zero count and piled into FAILED daily. When "🚨 SNS当日未出力: lane-X" arrives in Discord four days a week, a genuine outage alert one day gets skimmed past as "there it goes again." The moment alerts stop being trusted, your monitoring infrastructure is finished. After adding the branch that checks "does a log line for today exist?" with has today and routes lanes with no lines to UNKNOWN , my false-positive rate dropped by what felt like 90%+. Mixing UTC and JST creates a bug that only breaks at certain times of day. The gap between TODAY JST and TODAY UTC is 9 hours. If a job that runs between midnight and 9 AM Japan time aggregates with TODAY UTC , the string 2026-08-24 in the log matches the previous UTC date, and output completed overnight gets counted as "yesterday's success." From today's monitoring perspective it looks like zero, and a FAILED alert fires. Since it passes without issue when run during the day, it looks like "occasional false alarms" and the cause takes longer to find. I make this mixture explicit at the top of sns-output-watchdog.sh : TODAY JST="$ date '+%Y-%m-%d' " TODAY UTC="$ date -u '+%Y-%m-%d' " TODAY HUMAN="$ date '+%a %b %e' " launchdが出す形式。日は空白詰めなので %e Using the three variables appropriately and matching each log to the one it actually emits resolved it, but every time I add a new lane it needs re-checking. Discord notifications can silently fail to send. The notify function does -f "$DISCORD" || return 0 , so if discord tool.py doesn't exist, it exits successfully without notifying. You can end up in a state where the watchdog log correctly records result=unhealthy lanes=ig-1-likes0 but nothing arrives in Discord. I have a track record of the path being wrong on day one of deployment and notifications silently not going out. To verify that a monitoring script is actually raising alerts , you have to check on the receiving end Discord's last-received timestamp . Looking only at the watchdog log, you'll never notice. Nobody notices when a referenced log file goes stale. has today checks "is there a line for today?" but doesn't guarantee "has this file been updated recently?" Right after a job stops, has today also returns false and the lane becomes UNKNOWN — but the state where the file exists and has today's lines, i.e. "it ran once today, but every run after that has silently failed," is undetectable. Without a mechanism to periodically check the mtime of referenced logs, your monitoring falls into "continuing to judge today on stale evidence." Shared configuration across multiple repositories is "written only in the config file." Even though social-autolike/config/accounts.json says "reuseProfile": ".profiles/chrome-ig" , the code in scent-media/scripts/ensure chrome.sh contains no mention whatsoever that this profile is also used by another repo. In day-to-day work you look at git log / git diff separately for each, so collisions keep happening with no visible point of contact. I wrote this lesson into the learning note learning/shared-resource-kill-corrupts-the-neighbor.md : " Write it in the code of the side being shared, not the side doing the sharing " — that's the only way to minimize discovery cost. Among completed runs, "runs that genuinely accomplished nothing" need to be counted separately from zero output. If you count a run that terminated early on rate-limit but still managed 2 likes as a "dead run," you'll get an alert every night from mild nighttime rate limiting. That's exactly why count dead runs narrows to "a harmful termination reason need-login / circuit-break / error / rate-limit AND いいね:0." Changing that AND to an OR alone would send the false-positive rate through the roof. Here are the design principles I actually hit and fixed, in reproducible form. "Does CDP port 9223 respond?" is not a check for "is Chrome running" — it's a check for " is a Chrome that was launched in my particular style running ." A Chrome on the same profile launched by someone else exists without opening the port. Check process presence on a PID basis, like pgrep -f "user-data-dir=$PROFILE" , and don't depend on whether an interface responds. pkill -f