Skip to content

How it works

Everything hostsctl renders goes between two marker lines:

# >>> hostsctl begin >>>
# generated by hostsctl 2026-08-11 17:15
# source: /Users/you/.config/hostsctl/config.yaml — edit that, not this block
# --- local — Local development ---
127.0.0.1 k8s.orb.local # orbstack
# <<< hostsctl end <<<

Lines outside the markers are copied into the new file unchanged. That is the central invariant, and it is what the integration tests check against a copy of a real /etc/hosts: system lines, other tools’ blocks and hand edits all survive an apply.

If a begin marker is found with no end marker, the block is treated as running to the end of the file. Otherwise a truncated block would make every apply append a new one.

  1. Render the block from the config and the blocklist cache.
  2. Compose the new file: everything outside the markers, plus the new block in place of the old one (or appended, if there was none).
  3. Refuse if the result does not contain 127.0.0.1 localhost.
  4. Snapshot the current file into the backup directory.
  5. Write a temporary file next to the target, fsync it, copy the target’s mode and owner onto it, rename it over the target.
  6. Prune old snapshots, flush the DNS cache.

Step 5 is why an interrupted apply cannot leave a half-written /etc/hosts: rename on the same filesystem is atomic, so the file is either the old one or the new one.

The block carries a generation timestamp, but hostsctl keeps the old one when nothing else about the block changed. Without that, diff would report a change every minute, status would always demand an apply, and apply would rewrite /etc/hosts and create a backup for nothing. Keeping it also makes the timestamp mean what people assume it means: when the block last actually changed.

  • One line per address. A name on three addresses is three lines, because that is the only way /etc/hosts expresses it.
  • An exact repeat of an address-plus-name pair is dropped once, with a warning. Nothing else is dropped.
  • The same name in two groups is kept and warned about — it is legal, and usually a mistake.
  • A name that already appears outside the managed block gets a warning naming the file and line. hostsctl does not touch that line, and in /etc/hosts the first match wins, so an entry above the block quietly beats one inside it.

A block left by the older hosts-sync shell script is recognised but never removed on hostsctl’s own initiative. apply warns that it is there and that its entries may win; removing it takes an explicit --drop-legacy, or hostsctl migrate, which first imports the old files into the config. See Migration.

hostsctl check reports what the system would ignore rather than letting it fail silently — wildcards, a port or a path in a hostname, an address that does not parse, a name that is already defined outside the managed block. An error stops apply and exits 3; a warning is printed and nothing is blocked.

The full list, level by level, is in the validation rules.