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):
| ACTION | Typical meaning in practice |
|---|---|
add | Device appears (often alongside /dev node creation) |
remove | Device disappears |
change | State update; often raised by drivers; udevadm trigger defaults to change |
bind / unbind | Driver attaches or detaches |
move / online / offline | Rename / hotplug lock unlock edge cases |
Embedded pattern you will see:
- Cable insert → an
addevent (sometimes with siblings). - Firmware/attrs/permissions refresh, or someone runs
udevadm trigger→ achange. - 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 kind | ACTION strategy |
|---|---|
RUN+= script / external notify | Usually ACTION=="add" (plus optional remove) |
SYMLINK / MODE / OWNER / ENV / TAG | Skip remove or leave unrestricted; keep idempotent |
Re-apply via udevadm trigger | Default 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:
- Consecutive lines for the same
DEVPATH/DEVNAMEwith differentACTION. - Rising
SEQNUM(distinct events). - Match keys (
SUBSYSTEM,ID_VENDOR_ID, …) equal what your rule expects. - Split
--kernelvs--udevto 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) —
ACTIONmatch 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)