Skip to content
Documentation menu

Watch & Why

CLI

The 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:

why examples
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 %zone syntax • use --scope-id instead.
  • --all-addresses127.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.

watch examples
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 --json

Events

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

  • --interval accepts 100ms..=60s; the default is 1s.
  • --duration accepts 100ms..=7d. Without it, watch runs 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 false release events.
  • 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:

state: values
listen bound closed syn_sent syn_received established fin_wait1 fin_wait2
close_wait closing last_ack time_wait delete_tcb new_syn_received unknown

When 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.