Documentation menu
Watch & Why
CLIThe read-only observation commands • stream binds and releases, and get a real answer when a port refuses to bind.
Two commands that never signal anything. watch tells you what your ports are doing; why tells
you what’s stopping a bind right now. Both read the same native snapshot the rest of Kickoutchi
uses, and neither one runs Docker.
why • the real reason a bind fails
Address already in use is a symptom with at least four different causes. why separates them by
taking one snapshot, then actually attempting the bind and closing it immediately.
Note the two rows. 127.0.0.1:3000 is taken; [::1]:3000 is free. A tool that reports “port 3000
is busy” hides exactly that distinction.
Which endpoints get probed
The default query is TCP on 127.0.0.1, then ::1 • the two addresses a local dev server
usually means. Widen it explicitly:
kick why 3000
kick why 5353 --udp --address 127.0.0.1
kick why 3000 --tcp --address :: --ipv6-only
kick why 3000 --all-protocols --all-addresses --json--tcp/--udp/--all-protocols• protocol selection. TCP is ordered before UDP.--address ADDR• one literal local IP. No%zonesyntax • use--scope-idinstead.--all-addresses•127.0.0.1,0.0.0.0,::1, then::. It does not enumerate your interfaces.--scope-id ID• a nonzero IPv6 interface index; requires one explicit IPv6--address.--ipv6-only/--dual-stack• mutually exclusive, and valid only when every address is IPv6. With neither, you get the OS default.--reuse-address• probe with address reuse enabled.
The matrix is protocol-major and capped at eight endpoints.
Verdicts and certainty
Every endpoint gets a verdict and a certainty that belongs to that specific claim:
| Certainty | Means |
|---|---|
proven |
Directly observed • a probe result or a verified owner. |
estimated |
Derived from a bounded calculation, such as a Linux TCP timer. |
heuristic |
Inferred from a pattern rather than a fact. |
unknown |
Not established • usually a scope or permission limit. |
Verdicts distinguish bindable, occupied, permission denied, address unavailable, unsupported
behaviour, and other retained OS errors. Each one carries ordered evidence lines and, where
something couldn’t be established, gap lines naming what’s missing.
Exit codes
why uses the shared exit-code contract with one extra rule: for a
multi-endpoint query, the aggregate is the worst result present. Precedence is 1, then 4, then
3, and 0 only when every endpoint proved bindable.
watch • ports changing hands
watch polls full-state snapshots and emits an event whenever something changes.
kick watch
kick watch --tcp --address 127.0.0.1 --port 3000
kick watch --filter label:web --interval 500ms
kick watch --filter state:established --duration 30s --jsonEvents
| Event | Emitted when |
|---|---|
baseline |
The first valid snapshot, describing what was already open. |
bind |
An endpoint appeared. |
release |
An endpoint disappeared. |
replacement |
The same endpoint is now held by a different owner identity. |
collection_gap |
A poll failed. The last valid snapshot is kept rather than discarded. |
replacement exists because a PID alone isn’t identity. Kickoutchi compares PID plus a
start marker, so a recycled PID on the same port is reported as a replacement instead of silently
looking like nothing happened.
Timing
--intervalaccepts100ms..=60s; the default is1s.--durationaccepts100ms..=7d. Without it,watchruns until Ctrl-C.- Polls never overlap. If collection is slower than your interval, the next poll waits rather than building a backlog.
Failure and recovery
Collection can fail transiently. watch is explicit about it rather than fabricating events:
- A failed initial collection emits nothing and exits
1. - After a valid baseline, a failed poll emits
collection_gap, keeps the last valid snapshot, and retries. Recovery compares against that retained snapshot, so a transient failure can’t manufacture a wave of falsereleaseevents. - The third consecutive failure flushes its gap and exits
1. - Collection gaps bypass endpoint filters • you always learn that you stopped seeing.
Ctrl-C, duration expiry, and a closed stdout consumer (kick watch --json | head) all exit
successfully.
Filtering
watch accepts the shared filter grammar plus a watch-only state:
field covering every native TCP state:
listen bound closed syn_sent syn_received established fin_wait1 fin_wait2
close_wait closing last_ack time_wait delete_tcb new_syn_received unknownWhen a filter depends on a fact Kickoutchi couldn’t read • an owner PID, a process name • the event
is emitted with filter=indeterminate rather than being dropped. A definite non-match is still
suppressed. You are told when a filter might have hidden something.
Next
Both commands have machine-readable output. See Structured output for
the kickoutchi.watch_event/1 and kickoutchi.why/1 schemas.