Skip to content
Mobile
Agent

domain-context

Verified project intelligence for writing code: API quirks, platform behavior, and approaches that already failed. Read before working on the subsystem it covers. Feed new entries through the self-improvement protocol (AGENTS.md M5) when a session learns a durable project fact

BOOST
From plugin
zmninjang
1089 skills9 agents1 command
Install
$ npx -y skills add ZoneMinder/zmNinjaNg --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

Verified project intelligence for writing code: API quirks, platform behavior, and approaches that already failed. Read before working on the subsystem it covers. Feed new entries through the self-improvement protocol (AGENTS.md M5) when a session learns a durable project fact

Agent definition

domain-context.md

ZoneMinder domain context

Verified project intelligence for writing code: API quirks, platform behavior, and approaches that already failed. Read before working on the subsystem it covers. Feed new entries through the self-improvement protocol (AGENTS.md M5) when a session learns a durable project fact the hard way. Entries carry no personal data, hostnames, or addresses. If an entry stops matching reality, fixing it is a protocol change like any rule edit.

ZoneMinder API

  • Events index filters: `Id IN (...)` works; the `Id:csv` form does not.

`Tags.Id` accepts a single value only and cannot combine with `Id IN`. Repeating `MonitorId` params ORs them. Filter URLs cap out near 8KB; batch long id lists.

  • `monitors.json` returns the full monitor row to any account that can view

the monitor: `Path`, `User`, `Pass`, `ONVIF_Password`, `Options`. There is no per-field ACL, so an unprivileged account reads every camera's credentials straight from the API. Nothing app-side can prevent that; the app's job is to not widen it by logging or displaying those values (refs #307).

  • A camera password lives inside `Path` as URL userinfo

(`rtsp://user:pass@host/stream`), and pre-1.38 servers have no other field for it. `lib/security/url-credentials.ts` is the only place that knows how to find it; both the log sanitizer and the monitor settings UI go through it.

  • Alarm state (`monitors/alarm/id:{id}/command:status`) comes from the motion

score alone, never from recording mode. A `Recording=Always` monitor with an open `cause: Continuous` event still reports `0` (IDLE), verified against 1.39.18; it reports ALARM/ALERT on motion like any other monitor. `TAPE` (4) existed only before its removal in 1.37 dev, where a continuous recorder sat in it while quiet. So "always recording" never means "always alarming", and `isAlarmingState` covers the legacy case already.

  • Event Server v7.0.22 and later always sends a real `eid` in pushes. The

historical fake-eid bug (a `Date.now()` value where an event id belongs) was app-side tray handling, not the ES.

  • A zone's `Coords` are pixels or percent of the frame and no field says

which. The zone's `Units` column (`Pixels` / `Percent`) describes the analysis parameters (`MinAlarmPixels` and friends), not the coordinates, and its column default is now `Percent`, so a zone carried over from an older server reads `Units: Percent` over pixel coords. Trusting that field scales those coords by a hundred and throws the polygon off the frame. ZoneMinder's own renderer decides by magnitude, in `web/includes/Zone.php`'s `svg_polygon`: any point above 100 makes the whole zone pixels, everything at or below is percent. Match it, corner included (a pixel zone inside the top-left 100x100 reads as percent), so a zone looks the same here as in ZoneMinder's zone editor. 1.39.18 writes percent, so a full-frame zone reads `0,0 100,0 100,100 0,100`; percent values carry two decimals, so parse them as floats, and scale before rotation, which works in pixel space.

  • `ajax/control.php` has no denial branch. A PTZ command the account may

not send is answered with 200 and an empty body, so there is no refusal for the client to catch; PTZ is gated proactively on permissions or not at all (6161352f).

  • Control definitions arrive with their axis flags coerced to strings, and

a server may send a JSON boolean or an empty column, which lands as `'false'` or `''`. Gate capability on `=== '1'` rather than `!== '0'`, and treat only an absent field as capable (343bd334, 26054e6f).

  • A server that prunes deletes events underneath an open list, so a card

can outlive the row it describes and a PUT to that id answers 404. Treat that as the event being gone and refresh the list, not as a failed write (8002b74c). `isNotFound` lives beside `createHttpError`.

Streaming and media

  • Multipart MJPEG renders fine inside `<img>` on WKWebView (iOS and macOS)

and Chromium. The data-URL rendering workaround exists only for WebKitGTK on Linux (Tauri), where streaming leaks NetworkProcess memory and `ImageDecoder` is absent. Do not extend the workaround to other platforms.

  • Stream teardown sends `CMD_QUIT` for the previous connkey before starting

a new stream, on every path: unmount, profile switch, manual retry, and tab-visibility resume. Missing one path leaves stale ZMS processes on the server (ee8a7c9d, bef8c42d, e261e539).

  • `zms` exits mid-event when it cannot serve a frame, after painting the

reason ("Failed getting frame") into the MJPEG stream as its last frame. The `<img>` shows no error for that: it keeps the frame forever. The only client-side signal is the status query, which a connkey with no process behind it answers without `progress`/`duration`. Monitors that store video without JPEGs hit this most, since `zms` then has to decode the video.

  • `zms` serves `mode=single` out of shared memory without calling

`setLastViewed()`, so `zmc` sees no viewer. A monitor whose `Decoding` is `Ondemand` stops decoding ten seconds later, and every later snapshot repeats the same frozen frame (#383). `mode=jpeg&frames=1` runs one pass of the streaming loop, which does mark the monitor viewed, so snapshot polling uses it for `Ondemand` only. It is the expensive request: a zms streaming pass and a decoder wake per poll, and on a server with 74 `Ondemand` cameras it is what made the montage crawl (forcing `mode=single` on a device filled it markedly faster, #507). `Always`, `KeyFrames` and `KeyFrames+Ondemand` keep a recent picture decoded with nobody watching and stay on `mode=single`; keyframe tiles advance a keyframe at a time. Do not add a user switch that sends `mode=single` to `Ondemand`: it fills fast and then freezes (#509, reverted in #510). A missing `Decoding` field means a server older than the field, which is also older than `frames=`: unknown parameters are only logg

Read more
Ships withzmninjang

Documentation A ZoneMinder client for web, desktop, iOS, and Android. It shows live camera feeds, a montage of many cameras at once, and events on a timeline, and has an AI chat agent you can ask about your cameras.

Get the whole plugin

Other agents on zmninjang.

glossary
Agent

glossary

One name per concept, for code identifiers, briefs, docs, and commit messages. Each entry says what the thing is, not what it…