1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
|
---
title: Socket API
description: Control a running Herdr server from scripts, tools, and coding agents.
---
Herdr exposes a local socket API for scripts and agents that need to inspect or control a running session.
Most automation should start with the CLI wrappers. Use the raw socket API only when you need direct request/response control or long-lived event subscriptions.
## Choose an integration layer
| Layer | Use it for |
| --- | --- |
| Agent skill | Teaching a coding agent how to use Herdr from inside a pane. |
| CLI wrappers | Shell scripts, simple orchestration, and human debugging. |
| Raw socket API | Custom tools, protocol clients, and event subscribers. |
The layers share the same control surface.
## Schema
The installed CLI can print the socket protocol schema bundled with that Herdr
binary:
```bash
herdr api schema
herdr api schema --json
herdr api schema --output herdr-api.schema.json
```
Plain `herdr api schema` prints a short summary. `--json` prints the full JSON
Schema document for tools, and `--output PATH` writes that document to a file.
The schema covers raw requests, success responses, error responses, emitted
events, and subscription events.
## What you can control
The socket API can:
- create, list, focus, rename, and close workspaces
- create, list, focus, rename, and close tabs
- list, inspect, split, swap, focus, resize, rename, read, close, and send input to panes
- list, inspect, read, prompt, wait on, rename, focus, start, and attach agents through CLI helpers
- report custom agent state from hooks and plugins
- subscribe to events and wait for output or state changes
- install and uninstall built-in integrations
- stop the server and reload config
## CLI examples
Create a workspace:
```bash
herdr workspace create --cwd ~/project --label api
```
Create a tab:
```bash
herdr tab create --label logs
```
Split a pane and run a command:
```bash
herdr pane split w1:p1 --direction right
herdr pane run w1:p2 "npm test"
```
Inspect and rearrange panes:
```bash
herdr pane layout --current
herdr pane neighbor --direction right --current
herdr pane resize --direction right --amount 0.1 --current
herdr pane swap --direction right --current
herdr pane zoom --on --current
herdr pane split w1:p1 --direction right --ratio 0.333
```
Wait for an agent:
```bash
herdr agent wait w1:p1 --until done
```
Read pane output:
```bash
herdr pane read w1:p2 --source recent --lines 50
```
## Raw methods
Raw socket method names use dot notation:
| Area | Methods |
| --- | --- |
| Server | `ping`, `server.stop`, `server.reload_config`, `server.agent_manifests`, `server.reload_agent_manifests` |
| Notification | `notification.show` |
| Client | `client.window_title.set`, `client.window_title.clear` |
| Session | `session.snapshot` |
| Workspace | `workspace.create`, `workspace.list`, `workspace.get`, `workspace.focus`, `workspace.rename`, `workspace.move`, `workspace.report_metadata`, `workspace.close` |
| Worktree | `worktree.list`, `worktree.create`, `worktree.open`, `worktree.remove` |
| Tab | `tab.create`, `tab.list`, `tab.get`, `tab.focus`, `tab.rename`, `tab.move`, `tab.close` |
| Pane | `pane.split`, `pane.swap`, `pane.move`, `pane.zoom`, `pane.layout`, `pane.process_info`, `pane.neighbor`, `pane.edges`, `pane.focus_direction`, `pane.resize`, `pane.list`, `pane.current`, `pane.get`, `pane.rename`, `pane.send_text`, `pane.send_keys`, `pane.send_input`, `pane.read`, `pane.graphics.info`, `pane.graphics.set`, `pane.graphics.clear`, `pane.graphics.stream`, `pane.report_agent`, `pane.report_agent_session`, `pane.report_metadata`, `pane.clear_agent_authority`, `pane.release_agent`, `pane.close`, `pane.wait_for_output` |
| Popup | `popup.close` |
| Layout | `layout.export`, `layout.apply`, `layout.set_split_ratio` |
| Agent | `agent.list`, `agent.get`, `agent.read`, `agent.explain`, `agent.send_keys`, `agent.prompt`, `agent.wait`, `agent.rename`, `agent.focus`, `agent.start`, `agent.view.set`, `agent.view.clear` |
| Events | `events.subscribe`, `events.wait` |
| Integrations | `integration.install`, `integration.uninstall` |
| Plugins | `plugin.link`, `plugin.list`, `plugin.unlink`, `plugin.enable`, `plugin.disable`, `plugin.action.list`, `plugin.action.invoke`, `plugin.log.list`, `plugin.pane.open`, `plugin.pane.focus`, `plugin.pane.close` |
`agent.wait` is server-owned and event-driven. It pins the resolved pane occupant so a replacement cannot satisfy the wait. `agent.prompt` accepts an optional `wait` object with `until` and `timeout_ms`; this submits the prompt and starts the wait in one request, avoiding a race between separate calls.
`session.snapshot` returns a one-time bootstrap snapshot for clients that keep
their own local runtime cache. The response includes version/protocol metadata,
focused workspace/tab/pane ids, workspace records, tab records, pane records,
tab layout snapshots, and agent records. It is not a subscription; after reading
it, subscribe to resource events and update the local cache from those events.
Call `session.snapshot` again after reconnecting or when the local cache may be
stale. Attached worktree provenance is included on workspace records. Full repo
worktree discovery remains `worktree.list`.
From the CLI, `herdr api snapshot` prints the live `session.snapshot` response
as JSON for clients and agents that want a simple bootstrap command.
Pane control methods use public pane ids such as `w1:p1`. Methods whose
schema makes `pane_id` optional use the server's active focused pane when it is
omitted. `pane.move` always requires the source `pane_id`.
`pane.send_keys` and `pane.send_input.keys` accept Herdr key-combo strings:
plain printable keys, special keys like `enter` and `esc`, modifier chords
like `ctrl+h`, `control+j`, `alt+x`, and `shift+tab`, function keys like
`f1`, and named punctuation like `minus` and `plus`. They do not accept
`prefix+` binding strings.
```json
{"id":"req_current","method":"pane.current","params":{"caller_pane_id":"w1:p1"}}
{"id":"req_layout","method":"pane.layout","params":{"pane_id":"w1:p1"}}
{"id":"req_neighbor","method":"pane.neighbor","params":{"pane_id":"w1:p1","direction":"right"}}
{"id":"req_edges","method":"pane.edges","params":{"pane_id":"w1:p1"}}
{"id":"req_focus","method":"pane.focus_direction","params":{"direction":"right"}}
{"id":"req_resize","method":"pane.resize","params":{"pane_id":"w1:p1","direction":"right","amount":0.1}}
{"id":"req_zoom","method":"pane.zoom","params":{"pane_id":"w1:p1","mode":"toggle"}}
{"id":"req_split","method":"pane.split","params":{"direction":"right","ratio":0.333,"env":{"HERDR_ROLE":"tests"}}}
{"id":"req_process","method":"pane.process_info","params":{"pane_id":"w1:p1"}}
```
`pane.current` returns a single `PaneInfo`. When `caller_pane_id` is present,
Herdr returns that pane. When it is omitted, Herdr returns the active focused
pane.
`PaneInfo` includes `scroll` when terminal scroll metrics are available:
```json
{
"offset_from_bottom": 12,
"max_offset_from_bottom": 240,
"viewport_rows": 30
}
```
Clients can treat `offset_from_bottom == 0` as at-bottom state.
### Experimental pane graphics
Pane graphics let a plugin place image data over a pane. They are available
only when `[experimental].kitty_graphics = true`; otherwise every pane graphics
method returns `feature_disabled`. Calling `pane.graphics.info` returns the
attached client's cell width and height in pixels without creating a graphics
layer. `pane.graphics.set` accepts `png`, `rgb`, or `rgba` data in `data_base64`,
and `pane.graphics.clear` removes the layer.
```json
{"id":"graphics_info","method":"pane.graphics.info","params":{"pane_id":"w1:p1"}}
{"id":"graphics_set","method":"pane.graphics.set","params":{"pane_id":"w1:p1","format":"png","image_width":800,"image_height":600,"data_base64":"...","placement":{"viewport_col":0,"viewport_row":0,"grid_cols":80,"grid_rows":30}}}
{"id":"graphics_clear","method":"pane.graphics.clear","params":{"pane_id":"w1:p1"}}
```
For repeated frames, open a dedicated socket with `pane.graphics.stream`. After
Herdr replies with `ok`, send one JSON header and then exactly `data_length` raw
bytes per frame. A stream owns that pane's graphics layer until the socket
closes; concurrent set, clear, or stream requests return `stream_conflict`.
```json
{"id":"graphics_stream","method":"pane.graphics.stream","params":{"pane_id":"w1:p1"}}
{"format":"png","image_width":800,"image_height":600,"data_length":12345,"placement":{"viewport_col":0,"viewport_row":0,"grid_cols":80,"grid_rows":30}}
```
`pane.layout` returns the tab layout snapshot with `workspace_id`, `tab_id`,
`zoomed`, outer `area`, `focused_pane_id`, pane rects, and split rects/ratios.
`pane.neighbor` and `pane.edges` include that same layout snapshot so clients
can make the next decision without private layout state.
`pane.process_info` returns the pane's shell pid, foreground process group id
when available, and foreground processes with pid, name, argv/cmdline, and cwd
when the platform exposes them.
`layout.export` returns a portable tab layout tree. Omit `tab_id` and `pane_id`
to export the active tab, pass `tab_id` to export that tab, or pass `pane_id` to
export the tab containing that pane.
```json
{"id":"req_export","method":"layout.export","params":{"tab_id":"w1:t1"}}
```
The response includes `workspace_id`, `tab_id`, `zoomed`, `focused_pane_id`, and
`root`. `root` is a BSP tree of `pane` and `split` nodes. Pane nodes can include
`pane_id`, `label`, `cwd`, and argv `command`. Split nodes use
`direction` (`right` or `down`), `ratio`, `first`, and `second`.
`layout.apply` creates a fresh tab from a declarative tree. If `tab_id` is
provided, Herdr creates the replacement tab first and then closes the old tab.
This restores structure, labels, cwd, env, and optional argv commands; it does
not preserve live PTYs, scrollback, or running processes.
```json
{
"id": "req_apply",
"method": "layout.apply",
"params": {
"workspace_id": "wabc",
"tab_label": "dev",
"focus": true,
"root": {
"type": "split",
"direction": "right",
"ratio": 0.65,
"first": {
"type": "pane",
"label": "editor",
"cwd": "/repo"
},
"second": {
"type": "pane",
"label": "tests",
"cwd": "/repo",
"command": ["sh", "-c", "just test"],
"env": { "HERDR_ROLE": "tests" }
}
}
}
}
```
`layout.set_split_ratio` updates an existing split in a tab layout. The response
is `type: "layout_split_ratio_set"` with the updated portable `layout`.
```json
{"id":"req_ratio","method":"layout.set_split_ratio","params":{"tab_id":"w1:t1","path":[],"ratio":0.6}}
```
Process-launching methods accept an `env` object. Herdr applies those key/value
pairs to the newly launched process only. Herdr also injects `HERDR_SOCKET_PATH`,
`HERDR_ENV=1`, `HERDR_WORKSPACE_ID`, `HERDR_TAB_ID`, and `HERDR_PANE_ID` into
managed pane processes. Herdr-managed variables are authoritative when they
conflict with caller-provided env.
`pane.swap` supports directional and explicit forms:
```json
{"id":"req_swap_dir","method":"pane.swap","params":{"pane_id":"w1:p1","direction":"right"}}
{"id":"req_swap_explicit","method":"pane.swap","params":{"source_pane_id":"w1:p1","target_pane_id":"w1:p2"}}
```
Swap is same-tab only. It preserves split shape, split ratios, pane ids, and
running processes. The response is `type: "pane_swap"` with `changed`, optional
`reason`, `source_pane_id`, optional `target_pane_id`, `focused_pane_id`, and
`layout`. Reason values are `no_neighbor`, `same_pane`, `not_found`, and
`cross_tab`. When a tab is zoomed, swap keeps zoom active and mutates the hidden
full-tab layout.
`pane.move` moves a running pane to a different tab, a new tab, or a new
workspace:
```json
{"id":"req_move_tab","method":"pane.move","params":{"pane_id":"w1:p2","destination":{"type":"tab","tab_id":"w1:t2","target_pane_id":"w1:p3","split":"right","ratio":0.5},"focus":true}}
{"id":"req_move_new_tab","method":"pane.move","params":{"pane_id":"w1:p2","destination":{"type":"new_tab","workspace_id":"w1","label":"logs"},"focus":true}}
{"id":"req_move_new_workspace","method":"pane.move","params":{"pane_id":"w1:p2","destination":{"type":"new_workspace","label":"logs","tab_label":"main"},"focus":true}}
```
Existing-tab moves require `split: "right" | "down"`. `target_pane_id` is
optional and defaults to the target tab's focused pane. Same-tab layout changes
remain `pane.swap`; moving to the source tab returns `changed: false` with
`reason: "same_tab"`. Moves involving a zoomed source or target tab return
`changed: false` with `reason: "zoomed_tab"`.
The response is `type: "pane_move"` with `changed`, optional `reason`,
`previous_pane_id`, `previous_workspace_id`, `previous_tab_id`, the moved
`pane`, optional `source_layout`, `target_layout`, optional created workspace or
tab records, optional closed workspace or tab ids, and `focused_pane_id`.
Cross-workspace moves keep the internal pane and terminal alive but assign a new
public pane id in the destination workspace. Subscribers can listen for
`pane.moved`; Herdr does not emit fake pane close/create events for the moved
terminal process.
`pane.zoom` toggles, enables, or disables zoom for the target pane's tab:
```json
{"id":"req_zoom_toggle","method":"pane.zoom","params":{"pane_id":"w1:p1"}}
{"id":"req_zoom_on","method":"pane.zoom","params":{"pane_id":"w1:p1","mode":"on"}}
{"id":"req_zoom_off","method":"pane.zoom","params":{"pane_id":"w1:p1","mode":"off"}}
```
Omitting `pane_id` targets the server's active focused pane. The response is
`type: "pane_zoom"` with `changed`, `zoom_changed`, `focus_changed`, optional
`reason`, `pane_id`, `focused_pane_id`, `zoomed`, and `layout`. `changed` is
true when either zoom state or focus changed. Reason values are `single_pane`,
`already_zoomed`, and `already_unzoomed`.
The CLI wrapper for `notification.show` is:
```bash
herdr notification show "build failed" --body "api workspace" --position top-left --sound request
```
Show a user notification through the configured toast delivery:
```json
{"id":"req_notify","method":"notification.show","params":{"title":"build failed","body":"api workspace","position":"top-left","sound":"request"}}
```
`title` is required and must contain visible text after control characters and repeated whitespace are removed. `body` is optional. Herdr collapses newlines, tabs, carriage returns, and repeated whitespace into spaces, then trims notification text to 80 characters for `title` and 240 characters for `body`. An empty sanitized `title` returns `invalid_params`. `position` is optional and applies only when `ui.toast.delivery = "herdr"`; desktop positions are relative to the full Herdr frame, and omitted positions use `ui.toast.herdr.position`. Terminal, system, and off delivery ignore `position`. `sound` is optional and can be `none`, `done`, or `request`; it defaults to `none` and plays only when the notification is shown.
The response reports whether anything was shown:
```json
{"id":"req_notify","result":{"type":"notification_show","shown":true,"reason":"shown"}}
```
Possible reasons are `shown`, `disabled`, `rate_limited`, `no_foreground_client`, and `busy`. `disabled` means `ui.toast.delivery = "off"`. `busy` means an existing in-app toast was not replaced. Terminal and system delivery are best-effort through the current foreground attached Herdr client.
Set or clear the foreground client's outer terminal window title:
```json
{"id":"req_title","method":"client.window_title.set","params":{"title":"herdr api"}}
{"id":"req_title_clear","method":"client.window_title.clear","params":{}}
```
`client.window_title.clear` restores Herdr's default title. The response is
`type: "client_window_title"` with `changed` and reason `set`, `cleared`, or
`no_foreground_client`.
Worktree methods manage Git checkouts as Herdr workspaces. `worktree.create` creates a checkout and returns the new `workspace`, `tab`, `root_pane`, and `worktree` records. If the requested branch already exists locally, it checks out that branch; otherwise it creates the branch from the requested base or `HEAD`. `worktree.open` opens an existing checkout or returns the already-open workspace. `worktree.remove` runs `git worktree remove` against a linked child workspace and never deletes the branch.
Create a worktree from a source workspace:
```json
{"id":"req_1","method":"worktree.create","params":{"workspace_id":"w1","branch":"worktree/api","focus":false}}
```
Open an existing checkout:
```json
{"id":"req_2","method":"worktree.open","params":{"workspace_id":"w1","branch":"worktree/api","focus":true}}
```
Remove a linked checkout:
```json
{"id":"req_3","method":"worktree.remove","params":{"workspace_id":"2","force":false}}
```
Use at most one of `workspace_id` or `cwd` for `worktree.list`, `worktree.create`, and `worktree.open`; omit both to use the active workspace. Use exactly one of `path` or `branch` for `worktree.open`. Raw socket `cwd` and `path` values must be absolute; the CLI expands relative `--cwd` and `--path` values before sending requests. Workspace responses include optional `worktree` provenance when a workspace belongs to a Herdr worktree group. Worktree commands can emit `workspace.updated` when an existing workspace gains or changes worktree provenance.
Worktree commands also emit lifecycle events. `worktree.create` emits `workspace.created`, `tab.created`, `pane.created`, and `worktree.created`. `worktree.open` emits `worktree.opened`, and it also emits workspace/tab/pane creation events when it opens a new Herdr workspace. `worktree.remove` emits `worktree.removed`; if the linked workspace is still open, it also emits `workspace.closed`.
## Agent view queries
`agent.view.set` installs one transient declarative projection for the built-in
Agents view. The projection is reevaluated whenever agent facts or current UI
context change. It controls the expanded and collapsed sidebar, mobile Agents
list, mouse targets, indexed focus, and next/previous Agent navigation. It does
not change `agent.list`, notifications, detection, or global attention counts.
Show agents in the currently presented Space or agents needing attention
elsewhere, then order by attention and most recent state transition:
```json
{
"id": "view_set",
"method": "agent.view.set",
"params": {
"source": "plugin:example.agent-views",
"label": "focus",
"filter": {
"op": "any",
"filters": [
{
"op": "eq",
"field": "workspace_id",
"value": {"context": "current_workspace_id"}
},
{
"op": "in",
"field": "status",
"values": ["blocked", "done"]
}
]
},
"sort": [
{"field": "attention", "order": "desc"},
{"field": "state_change_seq", "order": "desc"}
]
}
}
```
Filter nodes use `op` values `all`, `any`, `not`, `eq`, `in`, or `exists`.
Built-in filter fields are `status`, `workspace_id`, `tab_id`, `pane_id`,
`agent`, `seen`, and `state_change_seq`. Use `{"token":"name"}` as a field to
filter plugin-reported pane metadata. Values are strings, booleans, unsigned
numbers, or a context object. Context values are `current_workspace_id` and
`current_tab_id`, and may only be compared to the matching ID field. Effective
status values are `idle`, `working`, `blocked`, `done`, and `unknown`; `done`
means idle and not yet seen.
Sort fields are `workspace_order`, `tab_order`, `pane_order`, `attention`,
`status`, `agent`, `seen`, `state_change_seq`, or `{"token":"name"}`. Sorts are
stable, evaluated in order, and accept `asc` or `desc`. Missing values stay
after present values. When `sort` is omitted, the existing
`ui.agent_panel_sort` policy remains active. A custom sort temporarily replaces
that policy without rewriting config.
`source` identifies the owner. Plugins use `plugin:<HERDR_PLUGIN_ID>`; Herdr
rejects plugin-owned sets when that plugin is missing or disabled. Other callers
may use their own non-`plugin:` source. A successful set atomically replaces the
previous view. The view lasts until it is cleared, replaced, its owning plugin
is disabled, unlinked, or uninstalled, or the server exits. Plugins that want
durable behavior should save the query under `HERDR_PLUGIN_STATE_DIR` and
reapply it from a `[[startup]]` hook.
Clear unconditionally, or only when the named source still owns the view:
```json
{"id":"view_clear","method":"agent.view.clear","params":{}}
{"id":"view_clear_owned","method":"agent.view.clear","params":{"source":"plugin:example.agent-views"}}
```
A source mismatch leaves the active view unchanged. Set and clear responses use
`type: "agent_view"` and report `active`, `source`, and optional `label`.
## Plugin APIs
The plugin API is an early host surface for executable workflow tools. A plugin
is a package with a `herdr-plugin.toml` manifest. The manifest declares
startup hooks, shareable actions, event hooks, terminal pane entrypoints, and
link handlers. Startup hooks run once after restore when the API is ready.
Actions and panes are manifest-only; runtime action registration and runtime
argv pane creation are not part of v1.
Installed and linked plugins persist across restarts. Herdr writes a
`plugins.json` registry file alongside `session.json` on `plugin.link`,
`plugin.unlink`, `plugin.enable`, and `plugin.disable`. The
`herdr plugin install` and `herdr plugin link` CLIs also write the same registry
when Herdr is not running, then startup loads it automatically. On startup,
Herdr re-reads each
manifest from its original path; if the file is missing or unparseable the entry
is kept with a `warnings` field so `plugin.list` surfaces it.
Event hook `on` values are validated against the known Herdr event names at link time. An unrecognised name is not an error — the link still succeeds — but the returned plugin info includes a warning (e.g. `"unknown event 'worktree.craeted'"`). Check the `warnings` field in the `plugin.link` and `plugin.list` responses.
Link a local plugin manifest:
```json
{"id":"req_plugin_link","method":"plugin.link","params":{"path":"/path/to/plugin","enabled":true}}
```
`plugin.link` also accepts optional `source` metadata. The CLI uses this when it installs from GitHub so `plugin.list` can show origin, requested ref, resolved commit, and managed checkout path:
```json
{"id":"req_plugin_link","method":"plugin.link","params":{"path":"/managed/plugin/herdr-plugin.toml","enabled":true,"source":{"kind":"github","owner":"ogulcancelik","repo":"herdr-plugin-examples","subdir":"worktree-bootstrap","requested_ref":"main","resolved_commit":"abc123","managed_path":"/data/plugins/github/<managed-checkout>","installed_unix_ms":1780000000000}}}
```
The path can be a plugin directory containing `herdr-plugin.toml` or a direct
manifest path. The manifest shape is:
```toml
id = "example.worktree-bootstrap"
name = "Worktree Bootstrap"
version = "0.1.0"
min_herdr_version = "0.7.0"
description = "Prepare new worktrees"
platforms = ["linux", "macos", "windows"]
[[build]]
command = ["bun", "install"]
[[actions]]
id = "bootstrap"
title = "Bootstrap worktree"
contexts = ["workspace"]
command = ["bun", "run", "bootstrap.ts"]
[[events]]
on = "worktree.created"
command = ["bun", "run", "bootstrap.ts"]
[[panes]]
id = "board"
title = "Worktree board"
placement = "overlay"
command = ["bun", "run", "board.ts"]
[[link_handlers]]
id = "github-issue"
title = "Open GitHub issue"
pattern = "^https://github\\.com/[^/]+/[^/]+/(issues|pull)/[0-9]+$"
action = "bootstrap"
```
`min_herdr_version` is required. The server refuses to link a plugin when the
field is missing, invalid, or newer than the running Herdr binary.
Declare `platforms` at the top level with the OS identifiers (`linux`, `macos`,
`windows`) your plugin supports. Omitting `platforms` is allowed for local
development — `plugin.link` succeeds but the response includes a warning.
Individual build commands, actions, event hooks, panes, and link handlers can
declare their own `platforms` to override the plugin-level list; if omitted they
inherit from the plugin. Invoking an action or opening a pane whose effective
platforms do not include the current OS returns a `platform_unsupported` error.
List, enable, disable, or unlink linked plugins:
```json
{"id":"req_plugin_list","method":"plugin.list","params":{}}
{"id":"req_plugin_disable","method":"plugin.disable","params":{"plugin_id":"example.worktree-bootstrap"}}
{"id":"req_plugin_enable","method":"plugin.enable","params":{"plugin_id":"example.worktree-bootstrap"}}
{"id":"req_plugin_unlink","method":"plugin.unlink","params":{"plugin_id":"example.worktree-bootstrap"}}
```
Actions are resolved from the linked manifest. `plugin.action.list` returns all actions across installed plugins; pass `plugin_id` to filter.
```json
{"id":"req_plugin_actions","method":"plugin.action.list","params":{}}
{"id":"req_plugin_actions_filtered","method":"plugin.action.list","params":{"plugin_id":"example.worktree-bootstrap"}}
```
`plugin.action.list` returns each action's effective `platforms` after applying
plugin-level inheritance.
Invoke an action by its qualified id or bare action id:
```json
{"id":"req_plugin_invoke","method":"plugin.action.invoke","params":{"action_id":"example.worktree-bootstrap.bootstrap","context":{"invocation_source":"keybinding"}}}
```
`plugin.action.invoke` resolves the manifest action, starts the manifest
command, and returns the Herdr-built invocation context plus the started command
log record. Missing context fields are filled from the active workspace, tab,
focused pane, worktree provenance, and request id. Invoking an action from a
disabled plugin returns a `plugin_disabled` error.
Herdr injects `HERDR_SOCKET_PATH`, `HERDR_BIN_PATH`, `HERDR_ENV=1`,
`HERDR_PLUGIN_ID`, `HERDR_PLUGIN_ROOT`, `HERDR_PLUGIN_CONFIG_DIR`,
`HERDR_PLUGIN_STATE_DIR`, `HERDR_PLUGIN_CONTEXT_JSON`, and available
`HERDR_WORKSPACE_ID`, `HERDR_TAB_ID`, and `HERDR_PANE_ID` values. Action
commands also receive `HERDR_PLUGIN_ACTION_ID`; event hooks receive
`HERDR_PLUGIN_EVENT` and `HERDR_PLUGIN_EVENT_JSON`; pane commands receive
`HERDR_PLUGIN_ENTRYPOINT_ID`.
List recent action and event command logs:
```json
{"id":"req_plugin_logs","method":"plugin.log.list","params":{"plugin_id":"example.worktree-bootstrap","limit":20}}
```
Event hooks run for enabled installed plugins when Herdr emits a matching event
name such as `worktree.created`.
There is no Herdr-managed plugin storage API in v1. `HERDR_PLUGIN_CONFIG_DIR`
and `HERDR_PLUGIN_STATE_DIR` are path discovery only; plugins own their files,
schemas, migrations, and cleanup.
Open a managed terminal UI:
```json
{"id":"req_plugin_pane","method":"plugin.pane.open","params":{"plugin_id":"example.board","entrypoint":"board","placement":"zoomed","target_pane_id":"w1:p1","env":{"HERDR_ROLE":"board"},"focus":true}}
```
`plugin.pane.open` requires an installed, enabled, platform-compatible plugin,
then launches the requested manifest `[[panes]]` entrypoint as an argv-backed
terminal pane. Manifest pane `placement` defaults to `overlay`; request
`placement` overrides the manifest with `overlay`, `popup`, `split`, `tab`, or
`zoomed`. Overlay and popup placements use the active tiled pane as launch
context. Popup terminals are session-modal and do not change the tab layout;
optional `width` and `height` fields set their outer size as terminal cells or
percentages such as `"80%"`. Omitted dimensions default to half the terminal
size, with too-small values clamped to the popup minimum. A popup has no pane
ID, remains outside all `pane.*` and agent APIs, emits no pane lifecycle events,
leaves plugin focus context on the underlying tiled pane, and does not export
`HERDR_PANE_ID` to its process. Popup launch returns `ok`; `popup.close` closes
the active popup and returns
`popup_not_open` when none exists. Split and zoomed panes target an existing
pane; tab panes can target a workspace. Split, tab, zoomed, and overlay panes
behave like normal Herdr panes, and `plugin.pane.focus` and `plugin.pane.close`
continue to operate on those panes.
## Socket transport
Herdr uses newline-delimited JSON over a local socket. On Unix, that socket is a
Unix domain socket. On Windows, it is a named pipe.
Send one request per line:
```json
{"id":"req_1","method":"ping","params":{}}
```
A successful response includes the same `id`:
```json
{"id":"req_1","result":{"type":"pong"}}
```
Event subscriptions keep the connection open after the initial response.
## Socket paths
The default socket lives under your Herdr config directory.
Named sessions have separate sockets:
```text
~/.config/herdr/herdr.sock
~/.config/herdr/sessions/<name>/herdr.sock
```
Resolution order:
1. explicit CLI `--session <name>`
2. `HERDR_SOCKET_PATH`
3. `HERDR_SESSION=<name>`
4. default session socket
Use `HERDR_SOCKET_PATH` only for low-level overrides.
For plugins, prefer invoking `HERDR_BIN_PATH` and the CLI wrappers when you need
portable Windows behavior. Raw socket clients are responsible for using the
platform-native local socket form.
## Agent state reporting
Integrations report agent state with `pane.report_agent`.
```json
{
"id": "req_1",
"method": "pane.report_agent",
"params": {
"pane_id": "w1:p1",
"source": "custom:docs",
"agent": "docs-bot",
"state": "working",
"message": "building docs"
}
}
```
`state` is semantic. It affects waits, notifications, and rollups. Report display-only values separately through metadata.
Session-only official integrations report native session references with `pane.report_agent_session`. State-reporting integrations can still include native session references in `pane.report_agent`. State-independent session reports do not affect waits, notifications, or rollups.
```json
{
"id": "req_2",
"method": "pane.report_agent_session",
"params": {
"pane_id": "w1:p1",
"source": "herdr:codex",
"agent": "codex",
"agent_session_id": "..."
}
}
```
`pane.get`, `pane.list`, `agent.get`, and `agent.list` expose a read-only `agent_session` object when Herdr has a stored native session reference:
```json
{
"agent_session": {
"source": "herdr:codex",
"agent": "codex",
"kind": "id",
"value": "..."
}
}
```
If no native session reference is stored, the field is omitted.
`pane.get`, `pane.list`, `agent.get`, and `agent.list` also expose `foreground_cwd` when Herdr can resolve the cwd of the process currently controlling the pane PTY. The existing `cwd` field remains the pane/workspace cwd used for labels, follow-cwd behavior, and restored session state.
`PaneInfo` and `AgentInfo` expose optional `terminal_title` and `terminal_title_stripped` fields. `terminal_title` is the latest OSC 0/2 title after safety normalization. `terminal_title_stripped` removes one recognized leading activity or spinner glyph and following whitespace. These server-owned values are ephemeral across a cold restart and are independent of the metadata `title` and semantic agent state.
Use `pane.report_metadata` when a user hook wants to customize presentation without taking over lifecycle state from a Herdr integration.
```json
{
"id": "req_2",
"method": "pane.report_metadata",
"params": {
"pane_id": "w1:p1",
"source": "user:claude-title",
"agent": "claude",
"title": "Refactor auth middleware",
"display_agent": "Claude: auth",
"state_labels": {
"working": "refactoring auth",
"idle": "ready",
"done": "review ready"
},
"tokens": {
"summary": "refactor auth",
"model": "opus"
},
"ttl_ms": 3600000
}
}
```
Metadata reports are display-only. Valid metadata can override the pane title, displayed agent name, visible state labels, and arbitrary named tokens. `working`, `blocked`, `idle`, waits, notifications, and rollups still come from semantic state. Native session restore comes from stored official session references. `agent` is an optional guard for presentation fields against the authoritative agent label; `applies_to_source` similarly guards presentation fields against the active lifecycle authority source. These guards do not apply to token patches: token reporters own clearing and TTL refresh. Use `display_agent` to change the visible name. `state_labels` keys must be `idle`, `working`, `blocked`, `done`, or `unknown`.
Token maps are per-resource patches. A string sets a key, JSON `null` clears it, and omitted keys remain unchanged. The latest accepted update wins. Optional TTL applies independently to token keys updated by that report. Pane tokens are exposed by pane and agent get/list responses and can be rendered as `$name` in Agent sidebar rows. A report may mention at most 16 token keys, and a pane or workspace may retain at most 32 keys. Token names are 1–32 ASCII letters, digits, underscores, or hyphens.
Workspace tokens use the same contract:
```json
{"id":"req_3","method":"workspace.report_metadata","params":{"workspace_id":"w1","source":"user:jj","tokens":{"jj_status":"2 changes","old":null},"ttl_ms":5000}}
```
Workspace get/list responses expose the resulting `tokens` map, and Space sidebar rows can render values such as `$jj_status`. Changes and TTL expiry emit `workspace.metadata_updated` with the latest workspace snapshot. This metadata event is available to API subscribers but does not invoke plugin event hooks.
Presentation text is normalized before storage. Herdr trims surrounding whitespace, removes control characters, and caps `title`, `display_agent`, each state label, and token values at 80 characters. Empty normalized token values clear that key.
`source` and `applies_to_source` are source identifiers. They must be 80 characters or fewer and may contain only ASCII letters, digits, colon, dot, underscore, and hyphen.
Use `ttl_ms` for short-lived metadata. It must be between `1` and `86400000` milliseconds. Omit `ttl_ms` for metadata that should stay until replaced, cleared, or the pane/workspace closes. Presentation fields retain their existing source-scoped expiry behavior; each token updated by the call receives its own deadline. Token metadata is not restored after a server restart.
Use `seq` when a hook may send updates out of order. For the same `source`, reports with a sequence number less than or equal to the last accepted sequence are accepted by the API but ignored by the pane state. A pane or workspace accepts sequenced token reports from at most 32 distinct sources during its lifetime; clearing or expiry does not release those source slots.
## Event subscriptions
Subscribe to events when you need a long-lived stream:
```json
{
"id": "sub_1",
"method": "events.subscribe",
"params": {
"subscriptions": [
{ "type": "pane.agent_status_changed", "pane_id": "w1:p1", "agent_status": "blocked" }
]
}
}
```
The first response acknowledges the subscription. Later lines are pushed events.
Workspace event subscriptions include `workspace.created`, `workspace.updated`, `workspace.metadata_updated`, `workspace.renamed`, `workspace.moved`, `workspace.closed`, and `workspace.focused`. `workspace.metadata_updated` reports token changes and TTL expiry without invoking plugin event hooks. Other workspace events describe Herdr UI/runtime lifecycle. `workspace.created` includes optional `workspace.worktree` provenance when the workspace belongs to a worktree group. `workspace.moved` includes the moved `workspace_id`, requested `insert_index`, and updated ordered `workspaces` list. `workspace.closed` includes a final `workspace` snapshot when Herdr can still identify it before removal.
Tab event subscriptions include `tab.created`, `tab.closed`, `tab.focused`,
`tab.renamed`, and `tab.moved`. `tab.moved` includes the moved `tab_id`,
`workspace_id`, requested `insert_index`, and updated ordered `tabs` list for
that workspace.
Pane event subscriptions include `pane.created`, `pane.updated`, `pane.closed`, `pane.focused`,
`pane.moved`, `pane.exited`, `pane.agent_detected`,
`pane.output_matched`, `pane.agent_status_changed`, and `pane.scroll_changed`.
Terminal-title changes can emit `pane.updated`, but spinner-only raw-title changes do not emit it when `terminal_title_stripped` is unchanged.
`pane.scroll_changed` is scoped to one `pane_id` and emits `pane_id`,
`workspace_id`, and the current `scroll` metrics whenever Herdr observes a
changed scroll snapshot.
Layout event subscriptions include `layout.updated`. The event carries the
updated `PaneLayoutSnapshot` for one tab. Clients that bootstrap with
`session.snapshot` should replace the cached layout with the same
`workspace_id` and `tab_id`.
Worktree event subscriptions include `worktree.created`, `worktree.opened`, and `worktree.removed`. Worktree events describe Git checkout lifecycle. `worktree.created` includes the opened `workspace` and created `worktree`. `worktree.opened` includes the target `workspace`, opened `worktree`, and `already_open`. `worktree.removed` includes the `workspace_id`, removed `worktree`, and `forced`.
Use `events.subscribe` for lifecycle events. Dedicated wait helpers are documented separately when a one-shot wait is supported.
## Reading panes
Use `pane.read` through the CLI unless you are writing a protocol client.
```bash
herdr pane read w1:p1 --source visible --lines 80
herdr pane read w1:p1 --source recent --lines 120
herdr pane read w1:p1 --source recent-unwrapped --lines 120
herdr pane read w1:p1 --source detection
```
`recent-unwrapped` is useful for logs because it ignores soft wrapping.
`detection` returns the bottom-buffer snapshot used by agent screen detection.
## Waiting for state
Use waits to coordinate agents and scripts.
```bash
herdr agent wait w1:p1 --until done
herdr agent wait w1:p1 --until blocked
```
Agent waits observe semantic state, not arbitrary command completion.
## Response shapes
Successful responses look like this:
```json
{
"id": "req_1",
"result": {
"type": "pane_info",
"pane": {
"pane_id": "w1:p1",
"terminal_id": "term_abc123",
"workspace_id": "w1",
"tab_id": "w1:t1",
"focused": true,
"agent_status": "working",
"revision": 42
}
}
}
```
`server.agent_manifests` returns the active agent detection manifest sources and remote update diagnostics without reloading rules:
```json
{
"id": "req_1",
"result": {
"type": "agent_manifest_status",
"last_check_unix": 1781043522,
"last_result": "checked",
"manifests": [
{
"agent": "cursor",
"source": "/home/me/.config/herdr/agent-detection/cursor.toml",
"source_kind": "local override",
"active_version": "2026.06.10.1",
"cached_remote_version": "2026.06.10.1",
"local_override_shadowing_remote": true,
"remote_update_result": "current"
}
]
}
}
```
Fields such as `last_check_unix`, `last_result`, `active_version`, `cached_remote_version`, `remote_update_result`, `remote_update_error`, `remote_last_checked_unix`, and `warning` are omitted when not available. `server.reload_agent_manifests` returns `agent_manifest_reload` with the same `manifests` item shape after reloading the in-memory rule cache.
`agent.explain` evaluates the target pane's detection snapshot in the running server using the server's active manifest cache:
```json
{
"id": "req_2",
"method": "agent.explain",
"params": { "target": "w1:p1" }
}
```
The response contains the same explain object printed by `herdr agent explain --json`, including the final state, manifest source and version, matched rule, evaluated rule evidence, skip-state reason, idle fallback reason, and `screen_detection_skip_reason` when a full lifecycle hook authority makes screen rules non-authoritative.
Clients need a running server that supports `agent.explain`; after upgrading Herdr, restart or live-handoff the server before relying on this method.
Errors look like this:
```json
{
"id": "req_1",
"error": {
"code": "not_found",
"message": "pane not found"
}
}
```
## Protocol stability
Herdr has a protocol version for client/server compatibility. Protocol changes are reviewed with release compatibility in mind.
Check the server protocol with `ping` or `herdr status` before depending on new behavior. Handle unknown fields gracefully.
|