udev ACTION=add vs change — Why Rules Run Twice

Plug in a USB, serial, or block device and a udev RUN/PROGRAM line may fire twice. Usually the rule matches both add and change (and sometimes bind) because ACTION was never narrowed. This post covers only add vs change · stopping double-runs · checking with udevadm monitor.

Grounding: the ACTION match key in udev(7) and the action list in udevadm(8) (add / remove / change / move / online / offline / bind / unbind). No affiliates or C++ samples.

What is the difference between add and change?

One-line answer: ACTION matches the event’s action name. add means the device appeared; change means the device state was updated (driver/kernel refresh, default for udevadm trigger, and more). They are separate events.

udev(7) says systemd-udevd receives uevents when devices are added, removed, or change state, then matches rules. The ACTION key is documented as: “Match the name of the event action.”

Common actions listed by udevadm(8):

ACTIONTypical meaning in practice
addDevice appears (often alongside /dev node creation)
removeDevice disappears
changeState update; often raised by drivers; udevadm trigger defaults to change
bind / unbindDriver attaches or detaches
move / online / offlineRename / hotplug lock unlock edge cases

Embedded pattern you will see:

  • Cable insert → an add event (sometimes with siblings).
  • Firmware/attrs/permissions refresh, or someone runs udevadm trigger → a change.
  • Physical devices may also emit bind/unbind.

A rule that only matches SUBSYSTEM/ATTR{idVendor} will run on every of those actions. Idempotent assignments (SYMLINK, MODE) are usually fine if they re-apply. RUN+= scripts, counters, or external side effects look like “it ran twice.”

Also note: udevadm test defaults to --action=add, while udevadm trigger defaults to change. Tests and live triggers are not the same action.

How do you stop the double-run?

One-line answer: Pin side-effect RUN/PROGRAM lines with ACTION=="add" (or the exact action you need). For properties, tags, and symlinks that should accumulate on every useful event, do not lock them to add only—prefer skipping remove, which matches systemd guidance around bind.

1) One-shot work → ACTION=="add"

# /etc/udev/rules.d/99-my-usb-once.rules
# Run only on plug (add) — skip change/bind re-entry
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1234", ATTR{idProduct}=="5678", \
  RUN+="/usr/local/bin/on-usb-plug.sh"

Cleanup on unplug belongs on a separate ACTION=="remove" line. Bundling RUN on add|change defeats “once.”

2) MODE / ENV / TAG: re-apply across events

Permissions and tags should often re-apply on change/bind. systemd 247-era guidance: replace old guards like ACTION!="add|change", GOTO="…_end" with ACTION=="remove", GOTO="…_end" so properties still accumulate on bind/unbind. An add-only guard can drop tags when only bind arrives.

# Properties/permissions: skip remove only (skeleton)
ACTION=="remove", GOTO="mydev_end"

SUBSYSTEM=="tty", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", \
  MODE="0660", GROUP="dialout", TAG+="uaccess"

LABEL="mydev_end"

3) Split by role

Rule kindACTION strategy
RUN+= script / external notifyUsually ACTION=="add" (plus optional remove)
SYMLINK / MODE / OWNER / ENV / TAGSkip remove or leave unrestricted; keep idempotent
Re-apply via udevadm triggerDefault action is change — add-only RUN will not fire

4) Reload

sudo udevadm control --reload-rules
sudo udevadm trigger   # default ACTION=change — do not expect add-only RUN

If you use trigger to “see the script again,” not running under an add-only rule is correct. Reproduce with a real plug or udevadm test --action=add.

Tip: Separate “my rule matched twice” from “the kernel emitted add then change (then bind).” The latter needs a narrower ACTION; the former suggests duplicate files or re-entrant IMPORT/PROGRAM.

How do you verify with udevadm monitor?

One-line answer: While plugging/unplugging, run udevadm monitor --environment --udev (and --kernel if needed) and watch for ACTION=add then ACTION=change on the same DEVPATH.

# Terminal 1 — post-udev events
sudo udevadm monitor --environment --udev

# Terminal 2 — plug/unplug the device, or
# sudo udevadm trigger -c add /sys/…   # force a specific action

Check:

  1. Consecutive lines for the same DEVPATH/DEVNAME with different ACTION.
  2. Rising SEQNUM (distinct events).
  3. Match keys (SUBSYSTEM, ID_VENDOR_ID, …) equal what your rule expects.
  4. Split --kernel vs --udev to separate raw uevents from udev-processed ones.

Dry-run matching:

sudo udevadm test --action=add /sys/devices/…/tty/ttyUSB0
sudo udevadm test --action=change /sys/devices/…/tty/ttyUSB0

test does not execute RUN; it shows which rules would match. If both add and change list your RUN, the rule likely omits ACTION==.

Caution: With OPTIONS+="watch", closing a node opened for write can synthesize a change uevent (udev(7) watch). That explains “change again with no trigger.”

Frequently asked questions

Is ACTION=="add|change" OK?
Fine for idempotent MODE/SYMLINK. For stopping RUN double-fire, it is usually the wrong tool. Prefer remove-only skip if you also need bind.

udevadm trigger and my script never runs.
Trigger defaults to change. An ACTION=="add" rule correctly skips. Use --action=add or a real plug.

add-only and permissions sometimes missing.
Some devices only finish binding after bind. Keep MODE/TAG on non-remove events; restrict RUN alone to add.

Edited rules.d but nothing changed.
udevadm control --reload-rules, then verify add-only with a plug or test --action=add. Files must end in .rules.

What should you remember?

ACTION names the event. add = appear, change = state update (and trigger’s default). Side-effect RUN → ACTION=="add" once; properties/tags → skip remove, re-apply idempotently. Prove it with udevadm monitor --environment. Sources: udev(7), udevadm(8). No affiliates.

Where are the official sources?

  • udev(7) — ACTION match key, RUN, OPTIONS=watch
  • udevadm(8) — monitor, trigger --action=, test --action= (add/remove/change/move/online/offline/bind/unbind)
  • udev — ArchWiki — action-type summary (secondary)