[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

Status Open
Reported on v2.1.270
Maintainer reply None cached
Activity 12 comments · opened Sep 14, 2026

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_ms schema 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 Monitor started 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

  1. Register a long-running Monitor intended to run for many hours (e.g. a persistent background poll loop), with timeout_ms: 86400000.
  2. Observe the call fails with InputValidationError: timeout_ms must be <= 3600000.
  3. Retry with timeout_ms: 3600000 (the allowed maximum). The call succeeds, but the confirmation text says "expires in 30m."
  4. Keep the session actively working (respond to normal messages, no idle period).
  5. After ~30 minutes, receive a task-notification saying the monitor "expired after 30m with no events delivered," even though the underlying watched process (and the session) were both still alive/active.
  6. Re-arm by calling Monitor again 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).

View original on GitHub ↗

12 Comments

Tekgnosis · 1 month ago

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.

hulm1818gm · 1 month ago

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:linux label 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 maximum has not.

From the sdk-tools.d.ts bundled with 2.1.272:

export interface MonitorInput {
  description: string;
  /**
   * Kill the monitor after this deadline. Default 300000ms.
   * Deadlines above 1800000ms are capped to 1800000ms.
   * You are notified at expiry and can re-arm.
   */
  timeout_ms: number;
  command?: string;
  ws?: { url: string; protocols?: string[] };
}

So as of 2.1.272 the description states the real 30-minute cap, while the validated maximum is still 3600000 — a value that is accepted but never honored. Confirmed empirically here: timeout_ms: 3600000 is accepted and still returns expires in 30m. That is exactly the discrepancy this issue asks to be removed; it now exists within a single type definition.

2. persistent survives only on the output side.

Same file:

export interface MonitorOutput {
  taskId: string;
  /** Timeout deadline in milliseconds (0 when persistent). */
  timeoutMs: number;
  /** No timeout — runs until TaskStop or session end. */
  persistent?: boolean;
}

MonitorInput has no persistent field at all, so it cannot be requested, while MonitorOutput still documents the mode. Combined with the public docs still listing persistent as 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.

travish-appfolio · 29 days ago

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 grep regex patterns. So the timing of writes is entirely outside the monitor's control.

As of 2.1.268 that premise breaks. persistent is 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's persistent was 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 persistent is 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.

treekmk · 29 days ago

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.

benpetermorris · 27 days ago

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.

jclerc · 26 days ago

Need this as well 👍

Auto-rearm is not a solution, it's a waste of token and less reliable than before :/

gtapps · 26 days ago

For context I did have a case already where the model didn't respect the rearm, I think I sent it via feedback

jrgilman · 22 days ago

Anthropic are you this desperate for us to burn tokens? Come on. No special flag to change this even?

hassaans · 22 days ago

Still reproduces on 2.1.280 (Linux).

A workaround that is not in this thread yet: monitors declared by a plugin in monitors/monitors.json still 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, print done):

  • Monitor tool, timeout_ms: 3600000: "expires in 30m", then [Monitor expired after 30m with 1 event delivered. ...]. done never printed.
  • Plugin monitor, "when": "always", loaded with claude --plugin-dir <dir>: still alive at 30:32, printed done at 40:00, ended with [exited with code 0]. No expiry notice.

Minimal version of the test plugin:

.claude-plugin/plugin.json   {"name": "monprobe", "version": "0.0.1"}
monitors/monitors.json       [{"name": "sleep2400", "description": "sleep 2400", "when": "always", "command": "sleep 2400; echo done"}]

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: true restored). If they are not, it would help to know before people move long-running watches onto plugins.

arcanemachine · 16 days ago

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.

savek-cc · 14 days ago

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 old persistent Monitor: 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.exe with --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.
  • Once a plugin monitor exits, Claude Code doesn't arm it again for the rest of the session. Invoking the skill again doesn't bring it back.

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:

context per request      ~75-95k tokens
cost per re-arm          $0.041 - $0.049
19 re-arms over ~9 h     $0.98  (a few with an extra status check)
one watch, per day       ~$2    (48 re-arms, more as the context grows)

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, honouring timeout_ms up to the 3600000 the schema already declares would at least halve the re-arms.

arcanemachine · 5 days ago

Not stale