[BUG] Monitor tool: doesn't respect persistent flag and timeout_ms schema caps at 3600000 but actual lifetime is ~30 minutes, even during active (non-idle) sessions
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet (related but distinct: #63023, #65968 — see below)
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
The Monitor tool now hard-rejects persistent and timeout_ms above 3600000 (1 hour) even with persistent set, it rejects with a schema validation error, and even a Monitor started with the maximum now allowed (3600000) is reported as expiring after only ~30 minutes — during a continuously active session, not an idle or paused one. It goes against the docs: [](https://code.claude.com/docs/en/tools-reference#monitor-tool)
Calling Monitor with timeout_ms: 86400000 (24 hours) fails immediately:
InputValidationError: [
{
"origin": "number",
"code": "too_big",
"maximum": 3600000,
"inclusive": true,
"path": ["timeout_ms"],
"message": "timeout_ms must be <= 3600000"
}
]
Retrying at the allowed maximum (timeout_ms: 3600000) succeeds, but the tool's own confirmation text says the monitor "expires in 30m," not 1h:
Monitor started (task <id>, expires in 30m unless the source ends first; you get one notice at expiry — re-arm if you still need the watch)
This is distinct from #63023 / #65968 (background tasks reaped on session pause/idle-suspend): my session never went idle — it was actively processing task-notification events roughly every 30 minutes, generated by this same monitor's own repeated expiry, for several hours straight. Re-arming immediately after each expiry produces the same ~30-minute lifetime again, indefinitely, regardless of session/user activity.
What Should Happen?
We should not have a limit on the monitor time, or bring back the old 24h limit....
Also either:
- The
timeout_msschema max should match the tool's actual honored lifetime (so a rejected value doesn't imply a longer-lived monitor is possible), and/or - A
Monitorstarted with any value up to the documented/allowed max should actually run for that long during active use, not silently cap out near 30 minutes.
At minimum the discrepancy between the accepted schema max (3600000) and the actual observed lifetime (~30 min) should not exist.
Error Messages/Logs
InputValidationError: [
{
"origin": "number",
"code": "too_big",
"maximum": 3600000,
"inclusive": true,
"path": ["timeout_ms"],
"message": "timeout_ms must be <= 3600000"
}
]
Monitor started (task <id>, expires in 30m unless the source ends first; you get one notice at expiry — re-arm if you still need the watch). You will be notified on each event. Keep working — do not poll or sleep. Events may arrive while you are waiting for the user — an event is not their reply.
[Monitor expired after 30m with no events delivered. Re-arm it if you still need the watch — and widen the filter if silence was unexpected.]
Steps to Reproduce
- Register a long-running
Monitorintended to run for many hours (e.g. a persistent background poll loop), withtimeout_ms: 86400000. - Observe the call fails with
InputValidationError: timeout_ms must be <= 3600000. - Retry with
timeout_ms: 3600000(the allowed maximum). The call succeeds, but the confirmation text says "expires in 30m." - Keep the session actively working (respond to normal messages, no idle period).
- After ~30 minutes, receive a
task-notificationsaying the monitor "expired after 30m with no events delivered," even though the underlying watched process (and the session) were both still alive/active. - Re-arm by calling
Monitoragain with the same command; the new monitor again expires after ~30 minutes, independent of user/session activity.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
Claude Code Version
2.1.270 (Claude Code)
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Non-interactive/CI environment
Additional Information
Related but distinct issues already open on this repo:
- #63023 — "Background agents silently die on session pause/resume" (framed around session pause/idle-suspend; I added a comment there with a shorter version of this data before realizing the mechanism differs)
- #65968 — closed as a duplicate of #63023, also framed around "idle/suspend boundaries"
Both of those describe reaping tied to session inactivity. This report is specifically about (a) a schema-level cap change that rejects previously-documented-as-valid timeout_ms values outright, and (b) the same ~30-minute cap applying even when the session is continuously active with no idle period — which doesn't fit the "idle/suspend" framing of the related issues, so I'm filing it separately in case the root cause differs (e.g. an unconditional per-monitor lifetime cap vs. an idle-detection reaper).
12 Comments
This change cost me actual money. I have a whole fleet of permanent monitors for an internal signaling application that reverted to the agents cache-busting themselves every 30m because the agents themselves noticed before I did. Extremely rude to remove a DOCUMENTED FEATURE in the previously working persistent flag.
This also impacts the agents default behavior of setting monitors on long running tasks for wakeup, such as training runs etc which now time out and cause cache busts every 30m for it to re-set the timer.
If something was intended to replace it that's fine, but the MODEL does not KNOW about it and continues to re-set defective monitor timers as it's default go-to action for long tasks that need wakeup.
Same behavior on 2.1.272 / Windows 11 / Claude Code subscription (not Anthropic API), so this does not appear to be Linux-specific — the
platform:linuxlabel may be misleading.Three data points that might help pin down the inconsistency:
1. The input schema's docstring has since been aligned to 30 min, but
maximumhas not.From the
sdk-tools.d.tsbundled with 2.1.272:So as of 2.1.272 the description states the real 30-minute cap, while the validated
maximumis still 3600000 — a value that is accepted but never honored. Confirmed empirically here:timeout_ms: 3600000is accepted and still returnsexpires in 30m. That is exactly the discrepancy this issue asks to be removed; it now exists within a single type definition.2.
persistentsurvives only on the output side.Same file:
MonitorInputhas nopersistentfield at all, so it cannot be requested, whileMonitorOutputstill documents the mode. Combined with the public docs still listingpersistentas a valid input parameter, this is where the confusion starts — it reads like a dropped parameter rather than an intentional removal.3. Reproduced the indefinite re-arm loop on an active session.
Four consecutive 30-minute expiries, session continuously working throughout (tool calls and user messages in between, no idle period, no suspend). Each re-arm produced the same ~30-minute lifetime. The watched process itself was healthy the whole time and was verified alive after each expiry.
Practical note for anyone hitting this: for the "wake me when something arrives" case, a script that polls and exits on the first event, started via Bash with
run_in_background, works around the cap — process exit re-invokes the session, and background processes are not subject to the Monitor lifetime. It trades continuous streaming for one wake-up per launch.I'm hitting this as well.
I've built a set of long-lived monitors — most of them simply tail append-only logs and wait for a signal that may not arrive for hours. The premise the whole workflow relies on is that the watch stays armed continuously.
The key detail: the log entries are written by a completely separate process. The monitor has no hand in producing them — all it does is tail the log files with various
grepregex patterns. So the timing of writes is entirely outside the monitor's control.As of 2.1.268 that premise breaks.
persistentis silently ignored and the watch is given a deadline + re-arm instead. When a watch expires and re-arms, anything the external process appends during the teardown/re-arm window is never delivered. There's no way for the monitor to "catch up" on it — the entry simply came and went while nothing was tailing.That's silent event loss, not just extra churn — and it isn't specific to log tailing. Any monitor observing a live stream (a long-running command's stdout, an event feed) has the same gap.
There's a cost dimension to this, too. It's already been raised above and it will increase my cost, but for a slightly different reason. I chose persistent monitors specifically to minimize tokens. The session is armed once and then only does work when a real event lands. Forced re-arming inverts that. I'd spend tokens on the arming itself on a fixed cadence, event or no event. I run a session per code review, each watching for events tied to that review. Across those sessions the re-arm overhead would dwarf the cost of the actual notifications the monitors exist to deliver. The mechanism I adopted to keep in-session notifications cheap becomes the thing driving cost up.
What makes this hard to work around is that there's no opt-out. The analogous WebFetch deadline added in the same release (2.1.268) kept an override (
CLAUDE_CODE_WEBFETCH_DEADLINE_MS=0). Monitor'spersistentwas silently disabled in 2.1.268 with no equivalent.If the goal was to bound runaway/idle watches, that's reasonable — but could the persistent/unbounded mode come back as an explicit opt-in (per-call flag or setting)? Those of us who need continuity can then accept the resource tradeoff knowingly, instead of losing events silently.
For now I'm pinned at 2.1.267. In my testing that's the last release where
persistentis actually honored (I've had a watch run continuously well past the 30-minute ceiling there). From 2.1.268 through at least 2.1.271 the flag is silently ignored and the watch is bounded + re-armed instead. I can't justify building a workaround without knowing whether an opt-in is likely to return, so until there's a signal on the intended direction, holding at 2.1.267 is the best option for me.I'm also disappointed to this Claude Code update, suddenly removing previously existing feature.
If this is not a mistake and intended update, the Claude Code team shall understand that users have developed skills, plugins, etc. in a way that has dependency to pre-existing features, and they should at least give proper migration guidance for breaking changes.
But considering the update period of Claude Code, I suspect it's not intended and some agent have just removed it without Claude Code team's intent. Well then, I hope them to recover this feature ASAP, and to have better reviewing & test pipeline to prevent this kind of event to recur.
This removal is a problem for my dev cycle, which uses a long-lived coordinator that emits events as the cycle progresses.
Claude doesn't reliably rearm the monitor after receiving the expiry notification. Why? Because it might be in the middle of something "more interesting", as it said itself when it diagnosed its own intermittent failures to act on the expiry notice.
Bottom line - relying on the model to rearm the watcher makes it vulnerable to attention issues and context rot.
Put the persistent flag back.
Need this as well 👍
Auto-rearm is not a solution, it's a waste of token and less reliable than before :/
For context I did have a case already where the model didn't respect the rearm, I think I sent it via feedback
Anthropic are you this desperate for us to burn tokens? Come on. No special flag to change this even?
Still reproduces on 2.1.280 (Linux).
A workaround that is not in this thread yet: monitors declared by a plugin in
monitors/monitors.jsonstill have no deadline. The plugins reference says they "run as a persistent background process for the lifetime of the session" (https://code.claude.com/docs/en/plugins-reference#monitors). Side by side on 2.1.280, same command shape (print a line,sleep 2400, printdone):timeout_ms: 3600000: "expires in 30m", then[Monitor expired after 30m with 1 event delivered. ...].donenever printed."when": "always", loaded withclaude --plugin-dir <dir>: still alive at 30:32, printeddoneat 40:00, ended with[exited with code 0]. No expiry notice.Minimal version of the test plugin:
Limits: plugin monitors run only in interactive sessions (not
-p), and the command is fixed in the manifest, so the model cannot start or retarget one. The script has to find its own targets and keep running.Question for the team: are plugin monitors meant to stay uncapped? If they are, the no-deadline watch still exists in the product, and the ask in this issue comes down to an opt-in for model-armed monitors (a setting, or
persistent: truerestored). If they are not, it would help to know before people move long-running watches onto plugins.For anyone reading this, you can get Claude to write a patch to fix the broken (?) monitor behavior.
Also commenting so this issue doesn't get auto-closed.
Two data points from Windows 11: Claude Code 2.1.284 (VS Code extension) and 2.1.285 (terminal CLI).
Plugin monitors and the VS Code extension
In the terminal CLI, a plugin monitor (
monitors/monitors.json) behaves like the oldpersistentMonitor: one task, alive for 35+ minutes, and it delivered an event at minute 35 without any re-arm.In the VS Code extension the same plugin never arms it - no task, no process, no error. The extension starts
claude.exewith--input-format stream-json --output-format stream-json, and as far as I can see in the bundled code, Claude Code arms plugin monitors only from an effect in the terminal UI. So extension users currently have no way to keep a watch alive past 30 minutes.Two traps if you go the plugin route anyway, both silent:
"when": "on-skill-invoke:..."needs the plugin-qualified skill name (on-skill-invoke:<plugin>:<skill>). The bare skill name never matches.Cost of the forced re-arm
Priced from one session transcript at API list price (Opus 5.5, cache reads at 5% of input). Each re-arm is a turn with two requests (call
Monitor, then the closing reply), and each request re-reads the whole context:An opt-in for a watch that lasts the whole session would fix both problems - a setting, an env var or a per-call flag (the WebFetch deadline got
CLAUDE_CODE_WEBFETCH_DEADLINE_MS). Short of that, honouringtimeout_msup to the 3600000 the schema already declares would at least halve the re-arms.Not stale