Skip to content
Documentation menu

Watch & Why

CLI

Probe a failed bind or stream socket changes without terminating processes.

why combines one process snapshot with a short-lived bind probe. watch compares successive native snapshots. Neither command sends signals or invokes 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 attempting the exact bind and closing it immediately.

kick why 3000
$ kick why 3000WHY port=3000 protocols=TCP addresses=127.0.0.1,::1 snapshot=partialscope kind=current_network_namespace identifier=net:[4026531833]tcp://127.0.0.1:3000 verdict=owned certainty=proven label=web dev  probe=address_in_use raw_os_error=98  evidence code=visible_verified_owner source=linux_procfs    PID 18422 (node) owned the endpoint at snapshot capture  evidence code=exact_bind_address_in_use source=bind_probe    the exact bind probe reported that the address is in usetcp://[::1]:3000 verdict=bindable_now certainty=proven label=-  evidence code=exact_bind_succeeded source=bind_probeaggregate_exit_code=3

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

CertaintyMeans
provenDirectly observed • a probe result or a verified owner.
estimatedDerived from a bounded calculation, such as a Linux TCP timer.
heuristicInferred from a pattern rather than a fact.
unknownNot 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.

An occupied endpoint is owned when a verified process holds it. It is owner_hidden when the holder can’t be seen, typically another user’s daemon that your account can’t read. On Linux the evidence then names the UID that created the socket, for example uid 970 created the socket. kernel_state_observed is reserved for sockets no process holds after every process was read, and for TCP states such as TIME_WAIT.

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 --tcp --port 3000
$ kick watch --tcp --port 3000BIND TCP 127.0.0.1:3000 listen owners=18422 label=web dev filter=matched certainty=provenRELEASE TCP 127.0.0.1:3000 listen owners=18422 label=web dev filter=matched certainty=proven
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

EventEmitted when
baselineThe first valid snapshot, describing what was already open.
bindAn endpoint appeared.
releaseAn endpoint disappeared.
replacementThe same endpoint is now held by a different owner identity.
collection_gapA 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.

That makes a narrow filter noisy on an unprivileged desktop: following one PID can also print every socket whose owner you can’t read. Add --matched-only to keep only definite matches. The trade-off is explicit: a match behind a permission boundary is then not reported. Collection gaps are always emitted.

definite matches only
kick watch --filter pid:18422 --matched-only

Next

Both commands have machine-readable output. See Structured output for the kickoutchi.watch_event/1 and kickoutchi.why/1 schemas.