agent-lookout 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -28,11 +28,11 @@
28
28
  document.documentElement.setAttribute("data-theme", theme);
29
29
  })();
30
30
  </script>
31
- <script type="module" crossorigin src="/assets/index-a7ubgAmE.js"></script>
31
+ <script type="module" crossorigin src="/assets/index-QSfCI-L_.js"></script>
32
32
  <link rel="modulepreload" crossorigin href="/assets/react-C2DiJaR6.js">
33
33
  <link rel="modulepreload" crossorigin href="/assets/motion-BAhdRuCw.js">
34
34
  <link rel="modulepreload" crossorigin href="/assets/radix-BH0JH8UU.js">
35
- <link rel="stylesheet" crossorigin href="/assets/index-BsTJp5-3.css">
35
+ <link rel="stylesheet" crossorigin href="/assets/index-B_1Its0R.css">
36
36
  </head>
37
37
  <body>
38
38
  <div id="root"></div>
package/docs/API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # API
2
2
 
3
- Agent Lookout's server answers a small HTTP API under `/api/`. The dashboard reads it, and so do `agent-lookout status` and `agent-lookout mcp`. It is meant for this computer only: it listens on a loopback address, and it answers only requests made to a loopback name. Every route returns JSON, and every route but six only reads.
3
+ Agent Lookout's server answers a small HTTP API under `/api/`. The dashboard reads it, and so do `agent-lookout status` and `agent-lookout mcp`. It is meant for this computer only: it listens on a loopback address, and it answers only requests made to a loopback name. Every route returns JSON, and every route but seven only reads.
4
4
 
5
5
  The answers hold your session names and folder paths. Check one before you share it.
6
6
 
@@ -31,33 +31,34 @@ curl -s http://127.0.0.1:4777/api/sessions
31
31
 
32
32
  Every answer has `Content-Type: application/json; charset=utf-8`, `Cache-Control: no-store`, `X-Content-Type-Options: nosniff` and `Cross-Origin-Resource-Policy: same-origin`. Every answer that is not a 200 has the body `{ "error": "<one sentence>" }`.
33
33
 
34
- | Status | When |
35
- | ------ | ----------------------------------------------------------------------------------------------------- |
36
- | 400 | The address, `since` or `windowMs` could not be read |
37
- | 403 | The `Host`, the `Origin` or `Sec-Fetch-Site` above |
38
- | 404 | There is no route at that path |
39
- | 405 | Any method but `GET`, with `Allow: GET`. The six routes that act take only `POST`, with `Allow: POST` |
40
- | 500 | Something went wrong that the server did not expect |
34
+ | Status | When |
35
+ | ------ | ------------------------------------------------------------------------------------------------------- |
36
+ | 400 | The address, `since` or `windowMs` could not be read |
37
+ | 403 | The `Host`, the `Origin` or `Sec-Fetch-Site` above |
38
+ | 404 | There is no route at that path |
39
+ | 405 | Any method but `GET`, with `Allow: GET`. The seven routes that act take only `POST`, with `Allow: POST` |
40
+ | 500 | Something went wrong that the server did not expect |
41
41
 
42
42
  ## Routes
43
43
 
44
- | Route | Answers |
45
- | ---------------------------------- | -------------------------------------------------------------------------------------- |
46
- | `GET /api/health` | `{ ok: true, version }` |
47
- | `GET /api/sessions` | The latest snapshot: `{ generatedAt, sources, sessions, answering, timeRules, quiet }` |
48
- | `GET /api/events?since=<epoch ms>` | `{ events }`, newest first, at most 200 |
49
- | `GET /api/history?windowMs=<ms>` | `{ points, startedAt, since, kept, restarts }`, one point for each poll, oldest first |
50
- | `GET /api/email` | `{ on, to, events, afterMs, asking, problem, last, limitedUntil }` |
51
- | `GET /api/webhook` | `{ on, host, events, afterMs, asking, problem, last, limitedUntil }` |
52
- | `GET /api/pull-requests` | `{ on, problem, gh, last }`: whether each branch's pull request is shown |
53
- | `GET /api/waits` | `{ at, today, sevenDays, since, where }`: how long sessions waited on you |
54
- | `GET /api/settings` | `{ timeRules, file, problem }`: the time rules, and the file they are kept in |
55
- | `POST /api/jump` | `{ ok: true, kind, place }`, with `app` for a terminal tab. One of six routes that act |
56
- | `POST /api/history/clear` | `{ ok: true, clearedAt }`. A route that acts |
57
- | `POST /api/sessions/stop` | `{ ok: true }` once the session's process has ended. A route that acts |
58
- | `POST /api/sessions/clean-up` | `{ results }`, what became of each session left running. A route that acts |
59
- | `POST /api/permission/answer` | `{ ok: true, decision }` once a permission prompt was answered. A route that acts |
60
- | `POST /api/settings/time-rules` | `{ ok: true, timeRules }` once the time rules are saved. A route that acts |
44
+ | Route | Answers |
45
+ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | `GET /api/health` | `{ ok: true, version }` |
47
+ | `GET /api/sessions` | The latest snapshot: `{ generatedAt, sources, sessions, answering, timeRules, quiet }` |
48
+ | `GET /api/events?since=<epoch ms>` | `{ events }`, newest first, at most 200 |
49
+ | `GET /api/history?windowMs=<ms>` | `{ points, startedAt, since, kept, restarts }`, one point for each poll, oldest first |
50
+ | `GET /api/email` | `{ on, to, events, afterMs, asking, problem, last, limitedUntil }` |
51
+ | `GET /api/webhook` | `{ on, host, events, afterMs, asking, problem, last, limitedUntil }` |
52
+ | `GET /api/pull-requests` | `{ on, problem, gh, last }`: whether each branch's pull request is shown |
53
+ | `GET /api/waits` | `{ at, today, sevenDays, since, where }`: how long sessions waited on you |
54
+ | `GET /api/settings` | `{ timeRules, file, problem, permissionRules, permissionRulesProblem, ruleAnswers, ruleAnswersSince }`: the time rules, the permission rules, the file they are kept in, and what the rules answered |
55
+ | `POST /api/jump` | `{ ok: true, kind, place }`, with `app` for a terminal tab. One of seven routes that act |
56
+ | `POST /api/history/clear` | `{ ok: true, clearedAt }`. A route that acts |
57
+ | `POST /api/sessions/stop` | `{ ok: true }` once the session's process has ended. A route that acts |
58
+ | `POST /api/sessions/clean-up` | `{ results }`, what became of each session left running. A route that acts |
59
+ | `POST /api/permission/answer` | `{ ok: true, decision }` once a permission prompt was answered. A route that acts |
60
+ | `POST /api/settings/time-rules` | `{ ok: true, timeRules }` once the time rules are saved. A route that acts |
61
+ | `POST /api/settings/permission-rules` | `{ ok: true, permissionRules }` once one change to the permission rules is saved. A route that acts |
61
62
 
62
63
  Times are milliseconds since 1970, and lengths of time are milliseconds.
63
64
 
@@ -146,6 +147,7 @@ Sessions come in this order: those that need you first, longest wait first, then
146
147
  | `jump` | `{ kind: "tmux", place }` or `{ kind: "terminal", app, place }` when `POST /api/jump` can take you to it |
147
148
  | `stop` | `{ how: "signal" }` or `{ how: "background" }` when `POST /api/sessions/stop` can stop it: a Claude Code session in a terminal or VS Code, or a background job. It holds nothing else |
148
149
  | `ask` | For `needs-you` Claude Code sessions with the Agent Lookout plugin only, while Agent Lookout holds the session's permission request: `{ requestId, tool, command, description, inputs, allow, denyOnly, subagent, until }`. `command` is the whole command for `Bash`, every line, and `inputs` is `[{ name, value }]`, each input as text, for any other tool or the rest of a command's. `allow` says whether `POST /api/permission/answer` takes `allow`, which it does only when the whole of what is allowed is here: never for `Edit`, `Write`, `MultiEdit` or `NotebookEdit`, and not for `ExitPlanMode` or `AskUserQuestion`, past 4,000 characters or 40 lines, with a character that cannot be shown as it is, with more than two blank lines in a row, or for a command holding right-to-left letters, with `denyOnly` saying why: `edit`, `too-long`, `hidden-characters`, `not-yes-or-no`, `right-to-left` or `blank-lines`. `until` is when the request is let go. Kept in memory only while it is held: no event, history point, email, webhook post or answer of `agent-lookout mcp` holds it |
150
+ | `answered` | `true` for a `needs-you` Claude Code session whose permission request Agent Lookout has answered, from Allow, Deny or a [permission rule](GUIDE.md#permission-rules), or is answering by a rule, while its registry file still says it waits in that wait, as it can for a second or two after the answer, and for 10 seconds after the answer at most. A session on another machine has it when Agent Lookout there sent it, for an answer given there. The wait is over: the dashboard's notifications and Agent Lookout's own, email and the webhook send nothing more for it, no reminder comes, `/api/waits` does not count it as waiting, `/api/history` counts it as working, not as needing you, and the dashboard, the Mac app's Dock badge and menu bar, `agent-lookout status` and the MCP tools count it as working too. Left out otherwise |
149
151
  | `stale` | `true` once it has been idle for 24 hours, or for as long as the idle rule in `timeRules` says while it is on. For a session on another machine too, by this computer's rule, whatever that machine said |
150
152
 
151
153
  | Source field | Holds |
@@ -193,11 +195,11 @@ With `AGENT_LOOKOUT_PULL_REQUESTS=on`, a session on a branch that has a pull req
193
195
  | `url` | Its page, `https://github.com/<owner>/<repository>/pull/<number>`, which the collector builds from the remote and the number rather than take from GitHub |
194
196
 
195
197
  It is what `gh` last said, asked at most once every 2 minutes for each repository and branch, so it can be up to about 2 minutes old. [Pull requests](GUIDE.md#pull-requests) in the guide says when it is asked for.
196
- `answering` says whether permission prompts can be answered from the dashboard: `{ state, plugin, holdMs, problem }`. `state` is `on` while Agent Lookout listens for the plugin's requests, `off` with `AGENT_LOOKOUT_ANSWER=off`, and `unavailable` when it could not listen, with `problem` saying why in one sentence. `plugin` is `seen` once a request has arrived since the last permission prompt it missed, `missed` when a Claude Code session waited for permission and no request arrived, as for a session without the plugin, and `unknown` before either. `holdMs` is how long a request is held.
198
+ `answering` says whether permission prompts can be answered from the dashboard: `{ state, plugin, holdMs, problem }`. `state` is `on` while Agent Lookout listens for the plugin's requests, `off` with `AGENT_LOOKOUT_ANSWER=off`, and `unavailable` when it could not listen or does not, as on Windows, which has no Unix socket for the plugin's hook, with `problem` saying why in one sentence. `plugin` is `seen` once a request has arrived since the last permission prompt it missed, `missed` when a Claude Code session waited for permission and no request arrived, as for a session without the plugin, and `unknown` before either. `holdMs` is how long a request is held.
197
199
 
198
200
  ### `GET /api/events?since=<epoch ms>`
199
201
 
200
- `{ "events": [...] }`: what changed, newest first, at most 200, and with `since` only those after it. Each event is `{ id, at, sessionId, sessionName, kind, from, to, severity }`, where `kind` is `appeared`, `status-changed`, `ended`, `stopped` or `answered`, `from` and `to` are statuses, and `severity` is `advisory`, `warning` for a change to `needs-you` or `critical` for a change to `failed`. A `stopped` event says that Agent Lookout stopped the session, when you pressed Stop or ended the sessions left running: it has `from`, the status the session had, no `to`, and `by: "agent-lookout"`. Its leaving the list is an `ended` event of its own. An `answered` event says that Agent Lookout answered a permission prompt of the session's, when you pressed Allow or Deny: it has `from`, `by: "agent-lookout"` and `decision`, `allow` or `deny`, and nothing of what was asked. A reader should pass over a `kind` it does not know: later versions may add others. The collector keeps the last 1,000 in memory, and with history kept on disk it reads them back when it starts again.
202
+ `{ "events": [...] }`: what changed, newest first, at most 200, and with `since` only those after it. Each event is `{ id, at, sessionId, sessionName, kind, from, to, severity }`, where `kind` is `appeared`, `status-changed`, `ended`, `stopped` or `answered`, `from` and `to` are statuses, and `severity` is `advisory`, `warning` for a change to `needs-you` or `critical` for a change to `failed`. A `stopped` event says that Agent Lookout stopped the session, when you pressed Stop or ended the sessions left running: it has `from`, the status the session had, no `to`, and `by: "agent-lookout"`. Its leaving the list is an `ended` event of its own. An `answered` event says that Agent Lookout answered a permission prompt of the session's, when you pressed Allow or Deny, or by a permission rule: it has `from`, `by: "agent-lookout"` and `decision`, `allow` or `deny`, and nothing of what was asked. One a rule gave also has `tool`, the tool's name as Claude Code gave it, and `rule`, `{ tool, command }`, the rule as you wrote it, with `command` only for a Bash rule that names one; its decision is the event's. Neither holds the command the session asked to run. A reader should pass over a `kind` it does not know: later versions may add others. The collector keeps the last 1,000 in memory, and with history kept on disk it reads them back when it starts again.
201
203
 
202
204
  ### `GET /api/history?windowMs=<ms>`
203
205
 
@@ -328,7 +330,7 @@ It never holds a token: `gh` signs in with its own login, which Agent Lookout ne
328
330
 
329
331
  ### `GET /api/settings`
330
332
 
331
- The settings Agent Lookout keeps itself, in a file on this computer, so they hold with no dashboard open: the [time rules](GUIDE.md#time-rules). It only reads.
333
+ The settings Agent Lookout keeps itself, in a file on this computer, so they hold with no dashboard open: the [time rules](GUIDE.md#time-rules) and the [permission rules](GUIDE.md#permission-rules), with what the permission rules answered since Agent Lookout started. It only reads.
332
334
 
333
335
  ```json
334
336
  {
@@ -344,20 +346,41 @@ The settings Agent Lookout keeps itself, in a file on this computer, so they hol
344
346
  }
345
347
  },
346
348
  "file": "~/.agent-lookout/settings.json",
347
- "problem": null
349
+ "problem": null,
350
+ "permissionRules": [
351
+ { "id": "3f9c2a71b0de", "decision": "deny", "tool": "Bash", "command": "git push --force:*" },
352
+ { "id": "8a41d07e6c2b", "decision": "allow", "tool": "Bash", "command": "npm test:*" },
353
+ { "id": "c5e0b9124fa3", "decision": "allow", "tool": "WebFetch" }
354
+ ],
355
+ "permissionRulesProblem": null,
356
+ "ruleAnswers": [
357
+ {
358
+ "at": 1791331869617,
359
+ "sessionId": "claude-code:00000000-0000-4000-8000-000000000001",
360
+ "sessionName": "checkout-flow",
361
+ "tool": "Bash",
362
+ "decision": "allow",
363
+ "rule": { "decision": "allow", "tool": "Bash", "command": "npm test:*" }
364
+ }
365
+ ],
366
+ "ruleAnswersSince": 1791328269617
348
367
  }
349
368
  ```
350
369
 
351
- | Field | Holds |
352
- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
353
- | `timeRules.longWait` | The long wait reminder: `on`, and `minutes`, a whole number from 1 to 1,440, how long a wait lasts before it is reminded of. 10 unless set |
354
- | `timeRules.idle` | The idle rule: `on`, and `hours`, a whole number from 1 to 720, how long a session is idle before it is stale. 48 unless set. While it is off a session is stale after 24 hours |
355
- | `timeRules.quietHours` | Quiet hours: `on`, `from` and `to` on the 24-hour clock as `HH:MM`, two different times, which run past midnight when `to` is the earlier, and `days`, the days they begin on, any of `mon` to `sun`. 22:00 to 08:00 every day unless set |
356
- | `quietHours.leaveOutAnswered` | Whether the summary sent when quiet hours end leaves out the waits that were answered while they held |
357
- | `file` | The file they are kept in, `~/.agent-lookout/settings.json` or the one `AGENT_LOOKOUT_SETTINGS_FILE` names, with your home folder written `~` |
358
- | `problem` | While the file could not be read when Agent Lookout started, or the last change could not be saved: one sentence that says why and names the file. `null` otherwise |
359
-
360
- Every rule is off until you turn it on. A rule the file holds in a form that cannot be read is off, and `problem` names it.
370
+ | Field | Holds |
371
+ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
372
+ | `timeRules.longWait` | The long wait reminder: `on`, and `minutes`, a whole number from 1 to 1,440, how long a wait lasts before it is reminded of. 10 unless set |
373
+ | `timeRules.idle` | The idle rule: `on`, and `hours`, a whole number from 1 to 720, how long a session is idle before it is stale. 48 unless set. While it is off a session is stale after 24 hours |
374
+ | `timeRules.quietHours` | Quiet hours: `on`, `from` and `to` on the 24-hour clock as `HH:MM`, two different times, which run past midnight when `to` is the earlier, and `days`, the days they begin on, any of `mon` to `sun`. 22:00 to 08:00 every day unless set |
375
+ | `quietHours.leaveOutAnswered` | Whether the summary sent when quiet hours end leaves out the waits that were answered while they held |
376
+ | `file` | The file they are kept in, `~/.agent-lookout/settings.json` or the one `AGENT_LOOKOUT_SETTINGS_FILE` names, with your home folder written `~` |
377
+ | `problem` | While the file could not be read when Agent Lookout started, or the last change could not be saved: one sentence that says why and names the file. `null` otherwise |
378
+ | `permissionRules` | The permission rules in force, in their order, each `{ id, decision, tool, command }`: `decision` `allow`, `ask` or `deny`, `tool` a tool's name as Claude Code writes it or `*` for every tool, and `command` only for a Bash rule that names one, exactly, `npm test`, or as a prefix, `npm test:*`. 100 at most. Empty is none |
379
+ | `permissionRulesProblem` | While the permission rules in the file could not be read whole, which leaves none in force, or the last change to them could not be saved: one sentence that says so. `null` otherwise |
380
+ | `ruleAnswers` | The requests the permission rules answered since Agent Lookout started, newest first, 100 at most, each `{ at, sessionId, sessionName, tool, decision, rule }`, with `rule` as it read then. Never the command the session asked to run |
381
+ | `ruleAnswersSince` | When Agent Lookout started, which is where `ruleAnswers` begins |
382
+
383
+ Every time rule is off until you turn it on, and there is no permission rule until you add one. A time rule the file holds in a form that cannot be read is off, and `problem` names it. Permission rules are read whole or not at all: a list that cannot be read whole is none in force, so an allow rule never answers what an ask rule beside it, unread, would have held back.
361
384
 
362
385
  ### `POST /api/settings/time-rules`
363
386
 
@@ -372,15 +395,38 @@ A route that acts. It puts the time rules in force and saves them in the setting
372
395
 
373
396
  The rules are in force once they are saved, and not before: the file is written whole, with mode 600, in a folder made with mode 700, and never through a link. Nothing in the request names a file.
374
397
 
375
- | Status | Body |
376
- | ------ | ----------------------------------------------------------------------------------------------- |
377
- | 200 | `{ "ok": true, "timeRules": { ... } }`: the rules now in force |
378
- | 400 | `reason: "invalid"`, with an `error` sentence that says what the body must be |
379
- | 500 | `reason: "not-saved"`: the file could not be written, as when it is a link, and nothing changed |
398
+ | Status | Body |
399
+ | ------ | ------------------------------------------------------------------------------------------------------- |
400
+ | 200 | `{ "ok": true, "timeRules": { ... } }`: the rules now in force |
401
+ | 400 | `reason: "invalid"`, with an `error` sentence that says what the body must be |
402
+ | 500 | `reason: "not-saved"`: the file could not be read or written, as when it is a link, and nothing changed |
403
+
404
+ ### `POST /api/settings/permission-rules`
405
+
406
+ A route that acts. It makes one change to the permission rules and saves the list in the settings file, as the Permission rules card in Settings does. It makes the same checks as `POST /api/jump`, with `X-Agent-Lookout-Action: permission-rules`, in `src/collector/settings/permissionRulesRoute.ts`, and takes a body of 1,024 bytes or less that is exactly one of:
407
+
408
+ ```text
409
+ { "add": { "decision": "allow", "tool": "Bash", "command": "npm test:*" } }
410
+ { "edit": { "id": "8a41d07e6c2b", "decision": "ask", "tool": "Bash", "command": "npm test:*" } }
411
+ { "move": { "id": "8a41d07e6c2b", "to": "up" } }
412
+ { "remove": { "id": "8a41d07e6c2b" } }
413
+ ```
414
+
415
+ A new rule goes to the end of the list, with an ID the server makes. A move at either end of the list changes nothing. The change is made to the list the settings file holds, read again just before, so one saved by another copy of Agent Lookout is kept. The server checks the rule itself, whatever the page checked, by `ruleProblem` in `src/core/permission-rules/permissionRules.ts`: a tool's name of letters, digits, `_` and `-` starting with a letter, or `*`; a command for Bash alone, in plain ASCII with one space between words, with no `;`, `&`, `|`, `<`, `>`, parentheses, quotes, backslashes, `$`, braces, and no `*` but a final `:*`; a command that begins with the program's name, not a variable set before it; and an allow rule that names one tool, never an MCP server alone such as `mcp__docs`, for Bash a command, and never `Edit`, `Write`, `MultiEdit`, `NotebookEdit`, `ExitPlanMode` or `AskUserQuestion`, whose command is one plain command that does not begin with a program that runs another, such as `sudo`, `env`, `xargs` or a shell (`COMMAND_RUNNERS` in `src/core/permission-rules/commandWords.ts`), or with one that sends requests or runs the code it is given, such as `curl`, `wget`, `nc`, `ssh`, `node`, `python` or `awk` (`NETWORK_CLIENTS_AND_INTERPRETERS` in the same file). The rules are in force once they are saved, and not before. Nothing in the request names a file.
416
+
417
+ | Status | Body |
418
+ | ------ | --------------------------------------------------------------------------------------------------------- |
419
+ | 200 | `{ "ok": true, "permissionRules": [ ... ] }`: the rules now in force |
420
+ | 400 | `reason: "invalid"`, with an `error` sentence that says what the body or the rule must be |
421
+ | 404 | `reason: "no-rule"`: no rule has that ID, as when another tab or another copy of Agent Lookout removed it |
422
+ | 409 | `reason: "full"`, the list holds 100 rules, or `reason: "duplicate"`, it holds that rule already |
423
+ | 500 | `reason: "not-saved"`: the file could not be read or written, and nothing changed |
424
+
425
+ A held permission request is put to the rules as its session's registry file confirms its wait, and a deny or an allow rule answers it through the same path as `POST /api/permission/answer`, below, with every check it makes. [Permission rules](GUIDE.md#permission-rules) says what each rule matches.
380
426
 
381
427
  ### `POST /api/jump`
382
428
 
383
- One of the six routes that act. It selects the tmux pane a session runs in, or brings its tab of Terminal or iTerm2 to the front, as the Jump button does. Its checks, on top of the ones every request passes, are `jumpRefusalFor` in `src/collector/jumpRoute.ts`, which makes those `actionRefusalFor` in `src/collector/handler.ts` makes for every route that acts. It answers only a request that:
429
+ One of the seven routes that act. It selects the tmux pane a session runs in, or brings its tab of Terminal or iTerm2 to the front, as the Jump button does. Its checks, on top of the ones every request passes, are `jumpRefusalFor` in `src/collector/jumpRoute.ts`, which makes those `actionRefusalFor` in `src/collector/handler.ts` makes for every route that acts. It answers only a request that:
384
430
 
385
431
  - is a `POST`. Any other method gets 405, with `Allow: POST`.
386
432
  - has an `Origin` that names this computer. A request with no `Origin` gets 403 here, though a read may go without one.
@@ -435,7 +481,7 @@ A route that acts. It ends one Claude Code session's process, as Stop session do
435
481
  - has `Content-Type: application/json`, or gets 415.
436
482
  - has a body of 1,024 bytes or less, or gets 413, that is exactly `{ "sessionId": "..." }`, or gets 400.
437
483
 
438
- The server looks the session up in its own latest snapshot, and stops only one with a `stop` field, by what it found for it itself: its process, or its background job. Before it acts it reads the session's registry file again, which must still name that session, that process, the same kind, `interactive` or `bg`, and the same start time, and asks `ps` for the process's start time again, which must be the same, to the second. It never stops process 1, its own process, the one it was started from or any process above that. Then it sends SIGTERM, and waits up to 10 seconds for the process to end, or runs `claude stop` with the job's ID. Nothing in the request reaches a signal or a command. A stop adds a `stopped` event, and the server reads the sessions again before it answers. It makes one stop a second, and one at a time, with the clean-up's.
484
+ The server looks the session up in its own latest snapshot, and stops only one with a `stop` field, by what it found for it itself: its process, or its background job. Before it acts it reads the session's registry file again, which must still name that session, that process, the same kind, `interactive` or `bg`, and the same start time, and asks `ps` for the process's start time again, which must be the same, to the second. It never stops process 1, its own process, the one it was started from or any process above that. Then it sends SIGTERM, or runs `claude stop` with the job's ID, and waits up to 10 seconds for the process to end. Nothing in the request reaches a signal or a command. A stop adds a `stopped` event, and the server reads the sessions again before it answers. It makes one stop a second, and one at a time, with the clean-up's.
439
485
 
440
486
  | Status | Body |
441
487
  | ------ | --------------------------------------------------------------------------------------------------------------------------------------- |
@@ -448,7 +494,7 @@ The server looks the session up in its own latest snapshot, and stops only one w
448
494
  | 429 | `reason: "too-soon"`, with `Retry-After: 1` |
449
495
  | 500 | `reason: "failed"`: the signal could not be sent, or `claude stop` failed |
450
496
 
451
- Each of these has an `error` sentence beside its `reason`. With `AGENT_LOOKOUT_STOP=off` there is no such route, and a POST to it gets 405.
497
+ Each of these has an `error` sentence beside its `reason`. With `AGENT_LOOKOUT_STOP=off`, or on Windows, there is no such route, and a POST to it gets 405. Windows has no `ps` to confirm a process's start time and no POSIX signal to send, so no session there has a `stop` field.
452
498
 
453
499
  ### `POST /api/sessions/clean-up`
454
500
 
@@ -476,7 +522,7 @@ with 1 to 20 sessions, none named twice, each with the `statusSince` the page sh
476
522
  }
477
523
  ```
478
524
 
479
- The answer is 200 with an `outcome` for each session asked for, in order: `ended`, `became-active`, `not-stale`, `gone`, `unsupported`, `cannot-confirm`, `not-allowed`, `still-running` or `failed`, which mean what the stop route's reasons do. One `stopped` event is added for each session ended. A clean-up while a stop or another clean-up is under way, or less than a second after one began, gets 429 with `reason: "too-soon"`.
525
+ The answer is 200 with an `outcome` for each session asked for, in order: `ended`, `became-active`, `not-stale`, `gone`, `unsupported`, `cannot-confirm`, `not-allowed`, `still-running` or `failed`, which mean what the stop route's reasons do. One `stopped` event is added for each session ended. A clean-up while a stop or another clean-up is under way, or less than a second after one began, gets 429 with `reason: "too-soon"`. With `AGENT_LOOKOUT_STOP=off`, or on Windows, there is no such route either, and a POST to it gets 405.
480
526
 
481
527
  ### `POST /api/permission/answer`
482
528
 
@@ -490,7 +536,7 @@ A route that acts. It answers one permission prompt of a Claude Code session, as
490
536
  }
491
537
  ```
492
538
 
493
- with `decision` `allow` or `deny`, and `requestId` the one the session's `ask` gave. Anything else gets 400. The server answers only a request it holds for that session under that ID, `allow` only when `ask.allow` is `true`, and only after it has read the session's registry file again and found it still says the session is waiting. It then hands the plugin's hook one of two fixed answers, allow, or deny with a sentence that says the person denied it from Agent Lookout, and never a rewritten input or a permission rule. An answer adds an `answered` event, and the server reads the sessions again.
539
+ with `decision` `allow` or `deny`, and `requestId` the one the session's `ask` gave. Anything else gets 400. The server answers only a request it holds for that session under that ID, `allow` only when `ask.allow` is `true`, and only after it has read the session's registry file again and found it still says the session is waiting. It then hands the plugin's hook one of two fixed answers, allow, or deny with a sentence that says the person denied it from Agent Lookout, and never a rewritten input or a permission rule. An answer adds an `answered` event, and the server reads the sessions again before it replies, so a `GET /api/sessions` sent once the reply has come finds the session gone on, or `answered`.
494
540
 
495
541
  | Status | Body |
496
542
  | ------ | -------------------------------------------------------------------------------------------------------- |
@@ -500,9 +546,9 @@ with `decision` `allow` or `deny`, and `requestId` the one the session's `ask` g
500
546
  | 409 | `reason: "gone"`: the session was answered there, is no longer waiting, or the request was held too long |
501
547
  | 429 | `reason: "too-soon"`: that request is being answered |
502
548
 
503
- Each of these has an `error` sentence beside its `reason`. With `AGENT_LOOKOUT_ANSWER=off` there is no such route, and a POST to it gets 405.
549
+ Each of these has an `error` sentence beside its `reason`. With `AGENT_LOOKOUT_ANSWER=off`, on Windows, or with an `AGENT_LOOKOUT_ANSWER_WAIT` it cannot read, `AGENT_LOOKOUT_CLAUDE_HOME` set without `AGENT_LOOKOUT_ANSWER_SOCKET`, or a socket path too long for a socket, there is no such route, and a POST to it gets 405. When the socket could not be opened as Agent Lookout started, as in a folder other users can open that it did not make, with something other than a socket at its path, or while another copy of Agent Lookout listens on it, nothing is held, and a POST that passes the checks above gets the same 405.
504
550
 
505
- The plugin's hook reaches Agent Lookout another way, not through this API: a Unix socket, `~/.agent-lookout/answer.sock` unless `AGENT_LOOKOUT_ANSWER_SOCKET` names another, of mode 600 in a folder of mode 700. It takes `POST /hooks/permission-request` with `X-Agent-Lookout-Hook: permission-request` and the hook's input as a JSON body of 1 MiB or less, holds it, and answers it with the decision JSON when you press Allow or Deny, or with an empty 200 when the request is let go. [Answer a permission prompt](GUIDE.md#answer-a-permission-prompt) says when that is.
551
+ The plugin's hook reaches Agent Lookout another way, not through this API: a Unix socket, `~/.agent-lookout/answer.sock` unless `AGENT_LOOKOUT_ANSWER_SOCKET` names another, of mode 600 in a folder of mode 700. It takes `POST /hooks/permission-request` with `X-Agent-Lookout-Hook: permission-request` and the hook's input as a JSON body of 1 MiB or less, holds it, and answers it with the decision JSON when you press Allow or Deny or a permission rule decides it, or with an empty 200 when the request is let go. [Answer a permission prompt](GUIDE.md#answer-a-permission-prompt) says when that is.
506
552
 
507
553
  ## In the Mac app only
508
554