udevmon
Run your own commands when a USB device is plugged in or removed, or when a removable filesystem is mounted or unmounted — configured entirely as a normal user, with no root privileges.
- Name your devices with a scan mode: plug something in, give it a name.
- Attach actions to those names: "when
backup-diskis mounted, run my backup script". - Identify devices reliably by serial number or filesystem UUID, not just by "some USB stick".
- Tiny footprint: one event-driven background process (about 20–35 MB of RAM, no polling, no CPU when idle).
How it works
udevmon listens to two sources, both available to unprivileged users:
- udev (device connect/disconnect), through the kernel's udev event stream.
- udisks2 (filesystem mount/unmount), through D-Bus. Most desktops already run it, and it is what automounts your USB sticks.
Concepts:
- device: A name you give to a physical USB device or a filesystem, plus the properties that identify it (e.g.
fs_uuid = "3F2A-91BC"). Devices are what you attach actions to. - event: Something that happened to a device:
connected,disconnected,mounted,unmounted,startup,shutdown. - tag: A label for a group of devices, so one action can cover several of them. A tag can be set on devices by hand, or selected automatically by a rule on properties (see Tags).
- action: A command to run when a given event happens to a given device (or to any device with a given tag).
Requirements
- Linux with systemd and udisks2 (standard on GNOME/KDE/Cinnamon desktops)
- Python 3.10 or newer (Ubuntu 22.04 and later, Debian 12 and later)
- Python packages
pyudevandPyGObject. On Debian/Ubuntu/Mint these are the system packagespython3-pyudevandpython3-gi, which are usually already installed. - On Python 3.10 only: the
tomlipackage (system packagepython3-tomli), whichpippulls in automatically when you install udevmon. - The
tomlkitpackage, version 0.11.7 or newer, except 0.15.0. Only the commands that change something already in your config (remove,tag,untag) use it; adding new entries,check,explainand the daemon don't.pippulls it in automatically. The system packagepython3-tomlkitis recent enough on Debian 13 and Ubuntu 24.04, but too old on Ubuntu 22.04 (it leaves stray lines in the file), wherepipfetches a current one instead.
Installation
udevmon is on PyPI. The simplest install is with pipx, for your user only (no root needed):
pipx install --system-site-packages udevmon
--system-site-packages lets udevmon use the system's python3-gi and
python3-pyudev (on Debian/Ubuntu/Mint: apt install python3-gi gir1.2-glib-2.0 python3-pyudev, usually already there). Without it, pip
builds PyGObject from source, which needs the GObject introspection
headers (libgirepository-2.0-dev, or libgirepository1.0-dev on older
releases; needs root), and PyGObject 3.51 and later need GLib 2.80 or newer.
On an older system (Ubuntu 22.04, for example) install 'PyGObject<3.51'
first.
A plain virtual environment works the same way:
python3 -m venv --system-site-packages ~/.venvs/udevmon
~/.venvs/udevmon/bin/pip install udevmon
(python3 -m venv needs the python3-venv package.) To install from a
source checkout instead, run pip install . in the project folder, or for
your user without a virtual environment:
pip install --user --break-system-packages .
--break-system-packages is required on Debian 12+/Ubuntu 23.04+ because of
PEP 668. It is safe here: --user installs into ~/.local and does not touch
system files.
pipx and pip install --user put the command in ~/.local/bin; make sure
it is on your PATH (a virtual environment has it in its own bin) and
check:
udevmon --version
Without installing, you can also run it straight from the source tree:
PYTHONPATH=src python3 -m udevmon ....
To remove it again, see Uninstalling below.
Tab completion (bash)
udevmon completion install
writes the script to ~/.local/share/bash-completion/completions/udevmon
(no root needed). Open a new terminal and press TAB. That location is loaded
automatically when the bash-completion package is installed, which is the
default on Ubuntu, Debian and Mint. Without it, put this in ~/.bashrc
instead:
source <(udevmon completion bash)
For zsh or fish, add --shell zsh or --shell fish. Fish loads
~/.config/fish/completions/udevmon.fish by itself. Zsh gets
~/.local/share/zsh/site-functions/_udevmon, and that folder must be in your
fpath (the command says how), or put source <(udevmon completion zsh)
in ~/.zshrc after compinit.
It completes commands, sub-commands and options, and it reads your config for the names that belong in each place:
- device names for
devices show|remove|tag|untagand--device; - tag names for
devices tag,--tag; ondevices untagonly the tags that device really has, and ontags removeonly declared tag rules; - action names for
actions remove; - event names as a list (
connected,disbecomesconnected,disconnected); - property names for
--match(rules leave out the ones that change from plug to plug), file names for--config, folders forservice install --dest.
If you pass --config PATH, the names come from that file. A missing,
invalid or unsafe config just means no names, never an error message. Each
TAB runs udevmon once (about 50 ms) and doesn't touch udev or D-Bus. It works
for the udevmon command, not for python3 -m udevmon.
Manual pages
pip install also installs two manual pages, udevmon(1) (every command and
option) and udevmon.toml(5) (every key of the configuration file, the events,
the properties and how commands are run):
man udevmon
man udevmon.toml
pipx and a user install put them in ~/.local/share/man, which man finds
when ~/.local/bin is on your PATH. A virtual environment keeps them in
its own share/man. From a source checkout, read them without
installing: man -l man/udevmon.1 and man -l man/udevmon.toml.5.
Uninstalling
Remove the extras first, then the package:
udevmon service uninstall # stop the daemon, remove the unit
udevmon completion uninstall # remove the tab-completion script
pipx uninstall udevmon # or: pip uninstall udevmon
service uninstallrunssystemctl --user disable --now, so the daemon stops cleanly (itsshutdownactions run), then deletes the unit file and reloads systemd.- Both commands only delete files udevmon wrote itself. A
udevmon.serviceor completion file that doesn't look like udevmon's is left alone and reported, and both commands are safe to run again. - With
--destthey only remove that file and never callsystemctl, mirroringinstall --dest. - Your configuration in
~/.config/udevmon/(and its.bak) is never touched. Delete it yourself if you no longer want it. - Uninstalling the package alone leaves the unit file and the completion script behind, which is why they come first.
The version number
udevmon --version prints a version taken from git, in the same format as
setuptools-scm, for example
0.1.dev6+gebbca8fea.d20260920:
0.1.dev6: 6 commits and no release tag yet. Once you tag a commit (saygit tag v0.2.0), that commit reads0.2.0, and later commits read0.2.1.dev3(3 commits after the tag).+gebbca8fea: the commit the build came from (gplus 9 hex digits)..d20260920: tracked files had uncommitted changes; the date is the build date. Untracked files don't count.
udevmon --version prints only the version, on one line. udevmon version
also lists the features scripts can test for (--json for both as JSON).
From a source checkout the version is asked from git each time. A copy that
was installed carries the version of the commit it was built from. Without
git the version comes from the source tarball's metadata, else it reads
0.0.0+unknown; the install never fails because of it.
Copying it to another machine
Build a wheel once, where the source is:
pip wheel . --no-deps -w dist/
then copy the .whl file to the other machine and install it there. That
machine needs neither the source nor git:
pip install --user --break-system-packages udevmon-*.whl
Add --no-deps if it has no network access and already has python3-pyudev
and python3-gi (and, on Python 3.10, python3-tomli; and a recent enough
python3-tomlkit) installed.
Quick start
1. Scan for a device and name it
udevmon scan
Plug in a USB stick (or unplug one, or mount/unmount a filesystem). udevmon prints what it sees, then after a short pause asks you what to do:
Detected:
[1] USB SanDisk Cruzer [0781:5567] unnamed
[2] Filesystem BACKUP (ext4) on /dev/sdb1 unnamed
Number(s) to name, iN to ignore, Enter = scan on, q = quit: 2
Register it as a [d]evice (one particular thing) or a [c]ategory
(a tag rule matching every thing like this; an action then
needs allow_unnamed = true to run for things matched only
this way)?
Choice [d] (q to skip this thing): d
Device name (Enter to skip): backup-disk
Properties you can match on: fs_uuid, fs_label, fs_type, devnode
Match on [fs_uuid] (Enter to accept, or type a list):
Tags, comma-separated (Enter for none): backup
About to save:
device 'backup-disk'
match: fs_uuid=3F2A-91BC (strong — identifies this exact device)
tags: backup
[Enter] save [r] rename [m] change match [t] change tags [c] cancel
>
Saved device 'backup-disk' to /home/me/.config/udevmon/config.toml
Add an action with e.g.:
udevmon actions add --device backup-disk mounted -- notify-send 'hello'
The prompts are asked in that order, and nothing is written until you confirm
the last screen: press Enter (or type y for a match that isn't strong) to
save, or fix something first — r to rename, m to redo the match, t to
redo the tags — and it asks again with what you typed kept. c (or n, q)
cancels the whole thing, and it stays in the list to pick again. If you type a
property that doesn't exist (say, a name at the "Match on" prompt), it tells
you and asks again. When nothing more is left to name it prints Waiting for more events and keeps scanning; press Ctrl+C to leave (the q answer works
only at the "Number(s) to name" prompt). To silence something you never want
to react to (a USB hub, an internal card reader), answer with i and its
number (i3, or i1,3): it goes straight to naming it as an ignored device,
without the device/category choice or tags (see
Switching things off).
A physical stick usually shows up twice: as a USB device (identified by vendor/product ID and serial number, when it has one) and as one or more filesystems (identified by UUID). Name whichever you want to react to. Name the USB device to react to it being plugged in at all; name the filesystem to react to it being mounted and unmounted.
Devices without a serial number, YubiKeys for one, can't be told apart, so
scan offers a category instead of a device: a declared tag with a rule,
picked by the same "this model, in this port" / "this model, in any port"
choice as a device match, but saved as [tags.NAME] rather than
[devices.NAME]. It is the default answer for such a thing:
Number(s) to name, iN to ignore, Enter = scan on, q = quit: 1
This thing has no serial number, so nothing tells one unit from another;
a category usually fits it better than a device, which claims to be one
exact, trusted thing.
Register it as a [d]evice (one particular thing) or a [c]ategory
(a tag rule matching every thing like this; an action then
needs allow_unnamed = true to run for things matched only
this way)?
Choice [c] (q to skip this thing): c
Category (tag) name (Enter to skip): yubikey
Properties you can match on: vendor_id, product_id, port_path, vendor, model
This thing has no serial number, so nothing identifies
this particular unit. Choose what to match on:
1) this model, in this port only (vendor_id, product_id, port_path)
2) this model, in any port (vendor_id, product_id)
Number, or a list of properties: 1
About to save:
category (tag) 'yubikey'
rule: vendor_id=1050 product_id=0407 port_path=3-4 (medium — ...)
[Enter] save [r] rename [m] change rule [c] cancel
>
Saved tag 'yubikey' to /home/me/.config/udevmon/config.toml
Add an action with e.g.:
udevmon actions add --tag yubikey connected --allow-unnamed -- \
notify-send 'hello'
A category never claims to identify one particular unit, so nothing runs for
it unless the action sets allow_unnamed = true (see
Named and unnamed things); the example command
above already includes it. d still registers it as an ordinary device if you
really mean this one physical thing (say, a key you keep in one USB port).
To also offer devices that are already plugged in, use udevmon scan --present.
When the daemon is running with the same configuration, scan shows the
events it receives (its banner says through the running daemon), so only one
process watches; otherwise scan watches by itself.
To see every property of what you plug in, the raw udev_* ones included (the
names you can use in a rule or as {udev_NAME} in a command), add --all-props;
udevmon present --all-props does the same for what is plugged in now. Properties
that change on every plug, which cannot be used in tag rules, are marked.
2. Attach an action
udevmon actions add --device backup-disk mounted -- \
notify-send "Backup disk ready" "{mountpoint}"
udevmon actions add --device backup-disk mounted \
--name run-backup --timeout 600 -- ~/bin/backup.sh "{mountpoint}"
The format is
actions add (--device NAME ... | --tag NAME ...) EVENT[,EVENT...] [--name NAME] [--timeout SECONDS] [--allow-unnamed | --named-only] -- COMMAND [ARGS...].
Options go before the --; everything after it is the command, verbatim.
--device and --tag can be repeated to target several devices or tags.
3. Run the daemon
Try it in a terminal first:
udevmon -v run
Plug and unplug the device and watch the log. Use udevmon run --dry-run to
log what would run without running anything.
4. Run it in the background
udevmon service install --now
This writes ~/.config/systemd/user/udevmon.service, enables it, and starts
it. From then on it starts with your login session. Useful commands:
systemctl --user status udevmon
journalctl --user -u udevmon -f # follow the log and action output
udevmon reload # re-read the config after editing it
systemctl --user restart udevmon
udevmon status says whether the daemon is running, with which
configuration, and how each action last ended (exit status, or why there is
none, and the last lines of a failure's output); it remembers only since the
daemon started. udevmon reload prints what the daemon loaded. If the configuration has
errors it prints them and fails (status 1, and systemctl --user reload udevmon fails too); the daemon keeps running with the previous one.
To only write the unit without enabling it, leave out --now.
Events
connected— a USB device is plugged in (USB properties).disconnected— a USB device is unplugged (USB properties).mounted— a filesystem is mounted (filesystem properties,mountpoint).unmounted— a filesystem is unmounted, or its device disappears while mounted (filesystem properties,mountpoint,surprise).removed— a mounted filesystem vanished without being unmounted first (the stick was pulled, or the drive failed). It comes right afterunmounted, which fires too (filesystem properties,mountpoint,surprise).startup— the device was already present when udevmon started (USB or filesystem properties).shutdown— the device is still present when udevmon exits (USB or filesystem properties).
Notes:
- Pulling a mounted stick without ejecting it produces
unmountedand thenremovedfor its filesystem, anddisconnectedfor the USB device. A normal eject produces onlyunmounted. Thesurprisevalue tells them apart:1for a pulled stick,0for a clean unmount, so one action onunmountedcan behave differently ({surprise}in the command,$UDEVMON_SURPRISEin the environment). To act only on pulled sticks, useon = ["removed"]. - A
removedaction cannot use the stick: it is already gone, so the mount point no longer exists and any command that reads or writes there fails.{mountpoint}is still filled in, but only as text (for a message or a log line). Use it to warn, log or clean up something of your own. - An event that repeats the last one of the same thing (the same plugged-in
device or filesystem, and the same kind) within one second is dropped
(
debounce, configurable), and the log says so. Two things that match one device are not repeats of each other, and any other event of the thing in between (mounted,unmounted,mountedagain) ends the repeat. shutdownactions run when udevmon receives a normal stop signal (systemctl --user stop, Ctrl+C, logout). The daemon waits for them to finish, up to each action'stimeout. They do not run if udevmon is killed hard (kill -9), crashes, or the machine loses power.startupfires for every matching present device each time the daemon starts, including aftersystemctl --user restart. A configuration reload (udevmon reload) does not trigger it.
Properties you can match on and use in commands
USB devices:
vendor_id,product_id— e.g.0781,5567; lowercase hex.serial— e.g.4C530001230815112433; best identifier, when the device has one.vendor,model— e.g.SanDisk,Cruzer Blade; human-readable.port_path— e.g.3-1.2; the physical port/hub position.devnode,syspath— e.g./dev/bus/usb/003/007.surprise— ondisconnectedonly:1if a filesystem on the device was still mounted when it was pulled,0otherwise.udev_<name>— e.g.udev_id_input_keyboard; every raw udev property, lowercased. In a command,{udev_id_model}is filled in from the event; whether a thing has the property is only known when it arrives, so the action is skipped, with an error in the log, for a thing that lacks it.checkcannot catch a typo there.
Filesystems:
fs_uuid— e.g.3F2A-91BC; best identifier.part_uuid— e.g.2b5c...; partition UUID (GPT disks).fs_label,fs_type— e.g.BACKUP,ext4; labels are not unique.devnode— e.g./dev/sdb1.mountpoint— e.g./media/me/BACKUP; only on mounted/unmounted/removed (and mounted startup/shutdown) events.surprise—1if the filesystem vanished while mounted,0after a clean unmount; only onunmountedandremoved(and on the USB device'sdisconnected, see above). It can be used in commands but not in tag rules.drive_vendor,drive_model,drive_serial— of the physical drive holding it.
On both kinds, added by udevmon itself:
instance— one value for each plug of a USB device (usb-9878235508) or each mount of a filesystem (fs-1758400000-3), identical in every event of it and new for the next plug or mount. See Matching an unplug with its plug. It changes on every plug, so it cannot be used in tag rules.
udevmon present lists everything currently present with these values, and
how strongly each could be matched.
Values are compared without regard to capitals (fs_label = "backup" matches a
drive labelled BACKUP), but otherwise exactly: no wildcards, and a thing that
lacks the property never matches.
Match strength
When you create a device, udevmon tells you how precisely it identifies things:
- strong: includes a serial number or a UUID. Identifies exactly this device.
- medium: model plus physical port (
vendor_id+product_id+port_path). Matches any device of that model in that port. It says where the device is plugged in, not which device it is, so it is never suggested for you; see Devices without a serial number. - weak: anything else, for example a filesystem label or just the model. Matches every device with those values.
Some cheap devices report fake serials (0000000, 1234567890, ...). udevmon
treats those as weak. Prefer strong matches for anything that runs a script.
Devices without a serial number
Some devices, YubiKeys for one, report no serial number, so nothing tells one unit
from another. When you name one with scan there is no pre-selected answer:
This device has no serial number, so nothing identifies
this particular unit. Choose what to match on:
1) this model, in this port only (vendor_id, product_id, port_path)
2) this model, in any port (vendor_id, product_id)
Number, or a list of properties:
Neither choice identifies the unit. 1 ties the device to that USB port: another
unit of the same model in that port matches too, and the device stops matching in
another port or behind a hub. 2 matches every unit of the model, in any port.
Pick the one that fits how the device is used, or type your own list. Both are
below strong, so scan asks before saving.
udevmon present marks these devices [medium: no serial, matches the port], and
udevmon check notes any device or tag rule that uses port_path, so a rule that
depends on a port is never a surprise. To tell such units apart you need something
outside udev, for example a script that asks the device itself.
Tags: one action for several devices
There are three ways to point one action at more than one device:
1. A list of devices
udevmon actions add --device yubikey --device card-reader \
connected,disconnected -- notify-send "{device} {event}"
2. Tags set on devices. A tag is a label; give it to as many devices as you like and target the label:
udevmon devices tag backup-disk backup
udevmon devices tag second-disk backup
udevmon actions add --tag backup mounted -- \
~/bin/backup.sh "{mountpoint}" "{device}"
(devices add ... --tag backup sets a tag when creating a device.)
Adding a third drive later is one command (udevmon devices tag third-disk backup);
the action doesn't change. A device can carry several tags, and an action can list
several tags (it runs if the thing has any of them). udevmon devices untag NAME TAG
removes one. Tags you set this way need no declaration.
3. Tag rules. A rule selects things by their properties, so you don't have to name each one. A value can be a list, meaning "any of":
udevmon tags add storage --match fs_type=ext4 --match fs_type=exfat
which is this in the config file:
[tags.storage]
match = { fs_type = ["ext4", "exfat"] }
Rules can use any property from the list above
except ones that change from plug to plug (mountpoint, devnode, syspath, and a few
raw udev ones); udevmon check rejects those. udevmon tags list and udevmon tags show NAME
show what each tag contains and which actions use it.
Wildcards. In a tag rule (not in a device's match), * stands for any run of
characters and ? for exactly one, and the whole value must match, ignoring
capitals:
udevmon tags add backups --match "fs_label=BACKUP*"
[tags.backups]
match = { fs_label = "BACKUP*" } # BACKUP, backup-2024, Backup_x ...
Nothing else is special (., [, ( mean themselves), so a label that really
contains a * or ? cannot be matched literally. A pattern can select many
things, so a rule that uses one is rated weak, like a list, and check notes it
for an allow_unnamed action. A * or ? in a device's own match is compared
literally, and check warns about it.
Several tags: any of, or all of
An action with several tags runs when the thing has any of them. To require all of them, say so:
udevmon actions add --tag backup --tag offsite --all-tags mounted -- \
~/bin/copy.sh "{mountpoint}"
udevmon actions set copy --all-tags # or --any-tags for the default
[[actions]]
tag = ["backup", "offsite"]
tag_mode = "all" # "any" is the default
on = ["mounted"]
run = ["~/bin/copy.sh", "{mountpoint}"]
The tags may come from any mix of explicit tags and rules. {tag} (and
$UDEVMON_TAG) is the first tag in the list. tag_mode only applies to actions
that use tag, and explain shows tags backup + offsite for such an action.
Opting a device out of a rule tag
A rule tag selects things you never named, which is what you want for most drives. A device of yours that a rule happens to select, but which should be left out, opts out of that tag:
udevmon devices exclude my-disk storage
udevmon devices include my-disk storage # back to normal
[devices.my-disk]
match = { fs_uuid = "3F2A-91BC" }
exclude_tags = ["storage"]
Now my-disk does not get the tag storage from its rule, so none of that tag's
actions run for it, but it is still a named device: it never falls back into
the unnamed category, and its own actions (device = "my-disk") still run. This is
different from status = "ignored", which silences the device completely.
An opt-out only stops a rule from selecting the thing. A tag the device carries
explicitly (tags = [...]) is never affected: listing the same tag in both
tags and exclude_tags is an error, and an explicit tag that another device
matching the same thing carries is kept. udevmon explain shows
excluded from: storage. devices add takes --exclude-tag TAG (repeatable).
udevmon check notes an opt-out with nothing to do: a tag without a rule, or a
rule that can never match the device (it needs another value for a key the
device's match names, such as fs_type).
Named and unnamed things
A rule can select things you never named, for example any ext4 drive someone plugs in. That is useful, but it is also exactly what "BadUSB" attacks rely on: the properties a rule looks at (filesystem type, vendor ID, label) are chosen by whoever built the device. So udevmon separates two cases:
- a named device (it matches one of your
[devices.*]), whether it got the tag explicitly or through a rule: every action on the tag. - unnamed (matches a rule only): only actions with
allow_unnamed = true.
Naming a device is you vouching for it. An action must opt in before it may run for hardware you haven't vetted:
[[actions]]
tag = "storage"
on = ["mounted"]
allow_unnamed = true # also runs for drives I never named
run = ["notify-send", "Drive mounted"]
[[actions]]
tag = "storage"
on = ["mounted"]
allow_unnamed = false # named devices only (the default)
run = ["~/bin/backup.sh", "{mountpoint}"]
On the command line: --allow-unnamed or --named-only on actions add --tag ....
Things to know:
allow_unnamedonly widens who may trigger an action. A named device is never restricted by it.- If you leave it out, the action behaves as
false, andudevmon checkprints a note for actions on rule tags so you make the choice on purpose. - Keep
allow_unnamed = trueactions harmless (notifications, logging). They run on hardware whose labels, model names and serial numbers are chosen by its maker, so don't let them read or run anything from the device.checkwarns when such an action uses those values in its command, or uses{device}(which doesn't exist for unnamed things). - Trust belongs to each thing, not to the whole stick: a stick appears as a USB device
and as a filesystem. If you named only the filesystem, its USB device is still
unnamed. In
scanyou can pick both entries at once (1,2) to name them together. startupandshutdownfollow the same rules, so an allowed action on a broad rule fires once for every matching thing that is present, including system filesystems.udevmon presentshows, for everything plugged in now, whether it is named and which tags it has, which is the quickest way to check a rule.
Variables per device
A device can carry values of its own, so one action can behave differently for each device it applies to. Instead of one action per drive:
[devices.photos]
match = { fs_uuid = "3F2A-91BC" }
tags = ["backup"]
vars = { dest = "/srv/backups/photos", keep = "30" }
[devices.music]
match = { fs_uuid = "7B10-44E0" }
tags = ["backup"]
vars = { dest = "/srv/backups/music", keep = "7" }
[[actions]]
tag = "backup"
on = ["mounted"]
run = ["~/bin/backup.sh", "{mountpoint}", "{var_dest}", "{var_keep}"]
{var_NAME} in a command is replaced by the value of the variable NAME of the
device the action runs for, and the environment gets UDEVMON_VAR_NAME. A
fourth drive is one more device; the action never changes.
udevmon devices add music --match fs_uuid=7B10-44E0 --var dest=/srv/m
udevmon devices set music --var keep=14 --unset-var old
--var NAME=VALUE sets (or changes) a variable and keeps the others; use it
more than once for several. Names are lowercase letters, digits and _.
-
A tag can set defaults that its devices override:
[tags.backup] vars = { keep = "7" }
An action with
taggets the defaults of the tag it runs through; an action withdevicegets those of the device's tags in the order itstagslists them (the first wins;checknotes two tags that disagree).udevmon tags set backup --var keep=7sets one. -
Unnamed things have only their tag's defaults. A thing matched only by a tag rule has no device, so an action that uses another variable is skipped for it, and
checkwarns when anallow_unnamedaction uses a variable none of its tags sets. -
A device that lacks the value is skipped with an error (as for any missing placeholder), never run with an empty value.
checkwarns about every device an action can reach that does not define a variable it uses, and rejects one that no device defines at all (usually a typo). -
The values are yours, not the hardware's, so unlike a label or serial number they count as safe in the checks for
allow_unnamedactions. -
When a thing matches several devices, the first one's variables are used.
-
devices showlists them, andexplainshows the command with them filled in.
Checking what will run
udevmon explain answers "what would happen if this were plugged in?"
without plugging anything in. It applies the same rules as the daemon (named or
unnamed, tags, allow_unnamed) and shows the commands with their values filled
in.
udevmon explain # everything plugged in now
udevmon explain mounted # only what "mounted" would do
udevmon explain connected --device stick
udevmon explain mounted --prop fs_type=ext4 \
--prop mountpoint=/media/x
With --device NAME it simulates that named device; --prop KEY=VALUE adds
properties (repeat it), so you can try an unnamed ext4 drive against a rule
without owning one. Only the properties you give are known. The output looks
like this:
Filesystem X (ext4) at /media/x unnamed tags: storage
mounted runs 'open' (tag storage, unnamed)
notify-send 'mounted /media/x'
mounted skips 'closed': an unnamed thing, and the action
does not set allow_unnamed = true
nothing to run for: unmounted, startup, shutdown
Things that match no device and no tag are left out (a count says how many);
--all lists them, and system filesystems too.
Running a device's actions now (trigger)
udevmon reload does not fire startup again, so a drive you name while it
is plugged in gets nothing until it is replugged. udevmon trigger asks the
running daemon to run a present device's actions now, as for a real event:
udevmon trigger key-daily # its startup actions
udevmon trigger key-daily mounted --wait # wait and show exit statuses
The event is startup (default), shutdown, connected (a USB device),
mounted (a mounted filesystem) or unmounted (a filesystem; it stays
mounted). The usual rules, locks, timeouts and placeholders apply, and the log
shows it like a real event, marked udevmon trigger. A disabled device's own
actions are skipped, as always. When it cannot trigger, it says why and the
exit status tells scripts which case it was:
| Status | Meaning |
|---|---|
| 0 | started (with --wait: every action exited with 0) |
| 1 | with --wait: an action failed, timed out or gave no status |
| 3 | the daemon is not running |
| 4 | the device is not in the running daemon's configuration |
| 5 | --config names another file than the daemon's |
| 6 | refused: not present, ignored, the event does not fit, or paused |
| 7 | the daemon's answer could not be read |
--json prints the result as one object, failures included, with an
error code and, with --wait, each action's exit status, why it has none
(timed out, could not start ...) and the last 10 lines of its output, so a
script can show why an action failed (man udevmon has the fields). The
text output shows those lines under a failed action. It talks to the daemon over
$XDG_RUNTIME_DIR/udevmon.sock, which only you can use. After an upgrade,
restart the daemon: an older one still running answers in its old protocol,
and trigger exits with 7 and says so.
Switching things off
You can stop things running without deleting them.
A device has a status. Left out (or "" or "active") it works normally.
disabled: the device's own actions, the ones that name it (device = "stick"), don't run. It is still recognised, so if it belongs to a tag, the tag's actions still run for it.ignored: nothing runs for it, whichever way an action would reach it (its own actions, tag actions,allow_unnamedones). If a thing matches two devices and either is ignored, nothing runs.
A disabled or ignored device still counts as named: it never falls back into
the unnamed category, where a rule tag could start running allow_unnamed
actions for it. startup and shutdown follow the same rules.
udevmon devices disable stick hub # own actions off, tags still apply
udevmon devices ignore hub # nothing runs for it at all
udevmon devices enable stick hub # back to normal
[devices.hub]
match = { vendor_id = "1d6b", product_id = "0002", port_path = "usb1" }
status = "ignored" # or "disabled"; leave it out for normal
An action can be disabled with enabled = false, or with
udevmon actions disable NAME and actions enable NAME. It stays in the file
and in udevmon explain, marked as disabled, so it is never a mystery why it
didn't run.
udevmon actions set NAME changes an action without removing and adding it
again:
udevmon actions set backup --timeout 900
udevmon actions set backup --on mounted,startup
udevmon actions set notify --allow-unnamed
udevmon actions set notify --named-only
udevmon actions set notify --default-unnamed # clear it: not stated
udevmon actions set notify --add-device card --remove-device stick
udevmon actions set archive --add-tag backup
udevmon actions set notify --timeout 30 -- notify-send "{device} {event}"
A command after -- replaces the action's command (the same as in actions add;
it must come last). The new command is checked like any other, so a placeholder
that the action's events don't provide ({mountpoint} on connected) is
refused and nothing is written. A comment after the old run line is kept.
--add-device, --remove-device, --add-tag and --remove-tag change what the
action targets, and can be repeated and combined with the other options. An
action targets devices or tags, never both, so the device options work on
actions that use device and the tag options on actions that use tag. A
device you add must exist; a tag needs no declaration. Removing something the
action does not target is an error, and so is removing its last target (use
actions remove for that). Adding what is already there changes nothing. A
single name stays a plain string, a list is edited in place, and comments and
layout are kept.
--default-unnamed removes the allow_unnamed line; the action then behaves as
named-only, and check notes it again if its tag has a rule. --allow-unnamed
and the other two apply only to actions that use tag.
The list commands, present, scan, explain and check show what is switched
off, and every one of these commands reloads a running daemon (--no-reload
skips it), like the other editing commands.
Renaming and changing things
udevmon devices rename stick usb-key
udevmon tags rename old-tag archive
udevmon actions rename backup nightly-backup
udevmon devices set usb-key --match vendor_id=1050 --match serial=Q1
udevmon devices set usb-key --description "Work key"
Renaming changes the name everywhere it is used, in one step:
- a device: its
[devices.NAME]header (and sub-tables such as[devices.NAME.match]) and every action that names it, whether asdevice = "NAME"or in a list, even one spread over several lines; - a tag: its
[tags.NAME]block, thetagsof every device that carries it, and thetagof every action that uses it (a tag you never declared can be renamed too); - an action: its
name.
The header stays where it was and comments and layout are kept. Before writing, udevmon checks that the result is exactly your old config with only that name changed, in the same order; if not, nothing is written. The command then tells you:
- which actions or devices it updated;
- which actions changed name because they have none of their own (an action
without a
nameline is called after its target, such asstick-4); - where the old name still appears in comments, descriptions or commands
(
run = ["echo", "stick"]). Those are left alone, since they may not mean the device.
Limits: a device written with dotted keys under [devices] instead of its own
[devices.NAME] table can't be renamed (it says so), and an action that has no
name line has to be given one first (edit the file).
devices set replaces a device's whole match (--match, repeated for
several keys; a device match takes single values, capitals ignored) and/or its --description
(empty removes it). A comment after the old match line is kept.
tags set changes a declared tag (one with a [tags.NAME] table):
udevmon tags set storage --match fs_type=ext4 --match fs_type=exfat
udevmon tags set storage --description "Filesystems I care about"
udevmon tags set storage --no-rule
--match replaces the whole rule (repeat a key to allow several values, as in
tags add), and --no-rule removes it, leaving a tag that only the devices that
carry it belong to. A comment after the old match is kept, and a rule that
uses a property changing from plug to plug is refused. A tag that only devices
carry has no table to edit: declare it with tags add. A new rule changes which
things the tag's actions run for, so run udevmon explain afterwards.
Names may contain letters, digits, _, . and -. A name with a dot is written
in quotes in the file ([devices."usb.1"]), which the commands handle for you.
Pausing everything
udevmon pause stops every action without touching your config, and
udevmon resume starts them again. Use it while you do a backup by hand, test
with sticks that would otherwise trigger actions, or give a demonstration.
udevmon pause # until you run `udevmon resume`
udevmon pause --for 30m # or for a while: 90s, 30m, 2h, 1d, 1h30m
udevmon resume
- While paused nothing runs, not even
startupandshutdownactions. The daemon keeps receiving events and logs them; events that happen during a pause are not replayed afterwards. - The pause is a small file in your runtime directory
(
$XDG_RUNTIME_DIR/udevmon.paused), so it survives a daemon restart but not a logout or reboot: a forgotten pause can't outlive your session. A pause with--forends by itself. - A running daemon is told at once and logs the pause. You can also pause when no daemon is running; it applies once one starts.
udevmon explainand the editing commands sayNote: automation is pausedso a paused daemon doesn't look broken, and the daemon's start-up log line shows[PAUSED].
Configuration file
The file is ~/.config/udevmon/config.toml (respects $XDG_CONFIG_HOME). Use
--config PATH to use a different one; note that --config goes before the
subcommand: udevmon --config my.toml run.
man udevmon.toml is the full reference for the file. You can edit it by hand
or through the CLI. CLI edits keep your comments and
formatting, and save the previous version as config.toml.bak.
-
CLI edits reload the daemon for you. After
devices add,actions removeand the other editing commands (and afterscansaves a device), a running daemon that uses the same config file picks the change up right away, and the command saysReloaded the running daemon.A daemon that uses a different config file is left alone. Add--no-reloadto skip it. -
Several edits as one change:
udevmon apply. Give it a JSON array of editing commands (from a file or standard input); they run in order on a copy, and only if all of them succeed are the files written, with one reload.--dry-runshows the whole change as one diff. A program that sets up a device together with its actions can use it so that a failure halfway leaves nothing behind:udevmon apply --dry-run <<'EOF' [["devices", "add", "key", "--match", "fs_uuid=3F2A-91BC"], ["actions", "add", "--device", "key", "mounted", "--", "backup.sh"]] EOF -
To edit by hand, use
udevmon edit(orudevmon edit 50-kp-syncfor a drop-in). Likevisudo, it opens a copy in$VISUALor$EDITORand, when you quit the editor, checks the whole configuration with your change. On errors you can edit again or discard. Otherwise it saves the file (keeping a.bak) and reloads the daemon. If another program changed the file in the meantime, it saves nothing and keeps your text asNAME.edit. -
After editing the file directly, run
udevmon check, thenudevmon reload. -
udevmon checkalso warns when an action's program can't be found or isn't executable (say, a mistyped script path). The daemon logs the same warning when it starts and on every reload, using its ownPATH, which under systemd is often shorter than your shell's (seescript_dirbelow).
[daemon] # every setting here is optional
debounce = 1.0 # ignore repeats within N seconds
max_workers = 4 # max commands running at the same time
udev_filters = ["usb/usb_device"]
script_dir = ["~/.local/udev/scripts"] # where bare command names live
[devices.backup-disk]
description = "Weekly backup drive"
match = { fs_uuid = "3F2A-91BC" }
tags = ["backup", "storage"] # optional explicit tags
[devices.yubikey]
match = { vendor_id = "1050", product_id = "0407", serial = "A1B2C3D4E5" }
[devices.card-reader]
match = { vendor_id = "2ce3", product_id = "9563", port_path = "3-3" }
[tags.storage] # a rule tag: things selected by properties
description = "Filesystems I care about"
match = { fs_type = ["ext4", "exfat"] } # a list means "any of"
# match = { fs_label = "BACKUP*" } # * and ? are wildcards in a rule
# A device can also have:
# exclude_tags = ["storage"] # opt out of a rule tag
# vars = { dest = "/srv/x" } # used as {var_dest} in commands
# and an action on several tags: tag_mode = "all" # default "any"
[[actions]] # one device
name = "run-backup" # optional
device = "backup-disk" # a name, or a list of names
on = ["mounted"] # one event or a list
run = ["~/bin/backup.sh", "{mountpoint}"]
timeout = 600 # kill after N seconds (default 60)
[[actions]] # several devices
device = ["yubikey", "card-reader"]
on = ["connected", "disconnected"]
run = ["notify-send", "{device} {event}"]
[[actions]] # a tag, including things I never named
tag = "storage" # a name, or a list (any of)
on = ["mounted"]
allow_unnamed = true
run = ["notify-send", "Drive mounted ({tag})"]
[[actions]] # a tag, named devices only
tag = "backup"
on = ["mounted"]
allow_unnamed = false
run = ["~/bin/backup.sh", "{mountpoint}", "{device}"]
An action has exactly one of device or tag. Tag names, device names and action names
use letters, digits, _, . and -.
Drop-in files (config.d)
Next to config.toml, every config.d/*.toml file is read too, after it and
in the order of the file names. A drop-in has the same tables as the main file;
its [daemon] may only have script_dir (see below). Its devices, tags and
actions join the others: an action in one file can use a device or a tag of
another.
This is for programs that install their own devices and actions: each owns a
file and replaces or deletes it whole, without touching your config.toml.
~/.config/udevmon/config.toml your own entries, read first
~/.config/udevmon/config.d/50-kp-sync.toml read second
~/.config/udevmon/config.d/90-rclone.toml read last
-
Order. The actions of one event run by their
order(see Running actions in a set order), and with the sameorderin the order the files are read: the main file's, then each drop-in's. One file order cannot put a tool's setup first and its teardown last, so between programs useorder, not file names. -
Names are shared. A device, declared tag or action defined in two files is an error that names both files (
udevmon checksays so; a reload that meets it keeps the previous configuration). -
Two devices for one thing. When one device's
matchcontains all of another's (or both are the same), whatever the first matches, the second does too.{device}of a tag action is then the one read first, so with the two in different files the file names decide it.udevmon checknotes every such pair; with one of themignored, it says nothing runs. It also notes two devices that match on different keys that one thing could have together (one onfs_uuid, the other ondrive_serial), since only the drive itself shows whether they meet. -
Its own
script_dir. A drop-in's[daemon] script_dirsays where its own actions' programs live, so a program that installs a drop-in can use bare names without asking you to editconfig.toml. Those folders are searched first, and only for the actions of that drop-in: they never change what a bare name means in your own actions or another drop-in's.# config.d/50-kp-sync.toml [daemon] script_dir = ["~/.venvs/tools/bin"]
-
Where things come from.
udevmon checklists the files it read;devices list,tags list,actions list,showandexplainmark the entries of a drop-in with its name. -
Editing.
devices add,tags addandactions addwrite toconfig.toml, or with--file NAMEtoconfig.d/NAME.toml(created if needed). Every other editing command changes the entry where it is defined, and a rename fixes the references in every file. -
The same safety rule applies: each file and the
config.dfolder must be owned by you and not writable by group or others. Hidden files and files not ending in.tomlare skipped. With--config my.tomlthe folder ismy.d.
How commands are run
runis an argument list, or a string split shell-style. It is never passed to a shell: no pipes, globbing or$VARIABLEexpansion in the command itself. If you need a shell, ask for one explicitly:run = ["sh", "-c", "cd \"$1\" && ls | wc -l >> ~/count.log", "sh", "{mountpoint}"]- A leading
~in an argument is expanded to your home directory. script_dir(under[daemon], a folder or a list of folders) says where your own scripts live. A command given by bare name (run = ["backup.sh", ...]) is then looked for in those folders, in order, and if it is in none of them, only in the standard system paths (/usr/local/sbin,/usr/local/bin,/usr/sbin,/usr/bin,/sbin,/bin). The daemon's ownPATHis not used, so the command is found the same way under systemd and in a terminal. The lookup happens at every event, so a script you add later is found without a reload, andudevmon explainshows the full path it will run. Withoutscript_dirnothing changes: bare names use thePATH. A command with a/in it is used as written. A drop-in can have ascript_dirof its own for its own actions (see Drop-in files).actions list --jsongives each action'sprogram, the file the daemon will start (withprogram_lookup:path,script_dir,PATHorplaceholder; for the last two only the daemon can tell, soprogramis null).udevmon checkwarns about a command found in none of them, and about a folder that does not exist, unless another folder of the list does: then a missing one is only a note (an optional folder this machine does not have).{var_NAME}is a variable of the device (see Variables per device).{name}is replaced by a property value:{mountpoint},{serial},{fs_uuid},{devnode}, and so on, plus{event},{device}(the name of the device that applies; not available for unnamed things) and{tag}(tag actions only). Values are always passed as a single argument, so spaces or shell characters in a mount point can't break out of the command.- Only
{lowercase_names}are placeholders. Other braces (${HOME},awk '{print $1}') are left alone. Write{{or}}for a literal brace. - If a placeholder isn't available for the event (for example
{mountpoint}on aconnectedevent),udevmon checkreports it. If it turns out to be missing at run time, the action is skipped with an error rather than run with an empty value. - The same information is available as environment variables:
UDEVMON_EVENT,UDEVMON_DEVICE(when the thing is named),UDEVMON_TAG(tag actions), andUDEVMON_<PROPERTY>(for exampleUDEVMON_MOUNTPOINT,UDEVMON_SERIAL).UDEVMON_SCRIPT_DIRis the folder of the program run, whenrun[0]is a path or was found inscript_dir(a relative path counts from your home folder), so a script finds the files next to it; it is not set for a bare name left toPATH. - Graphical programs work under the service too. For every action udevmon
looks up your desktop session's
DISPLAY,WAYLAND_DISPLAY,XAUTHORITYandDBUS_SESSION_BUS_ADDRESS: from systemd's environment for your user, or, if your desktop never put them there, from one of your own running programs. If the service started at the very beginning of your login, before the desktop has a display, an action waits for it (up to 30 seconds). So it does not matter whether the service started before or after you logged in. Run from a terminal that has a display, your own environment is used as it is. - Commands run as you, with your home directory as working directory and no standard input. Their output goes to the log, line by line as it arrives, each line prefixed with the action's name.
- Several actions for one event run one after another, by
order(see below), then in file order. A failing action does not stop the next. - An action that runs longer than its
timeoutis terminated (along with any child processes it started).
Programs that keep running
udevmon waits for the command to exit, not for everything it started. A script
that does its work, starts a program in the background (keepassxc &, then
disown) and exits, is finished when it exits: the program keeps running, and
whatever it writes to its output is still logged. The timeout only applies while
the script itself is running.
When the command is the long-running program, say so with detach:
udevmon actions add --device backup-disk mounted --detach -- \
keepassxc "{mountpoint}/pass.kdbx"
[[actions]]
device = "backup-disk"
on = ["mounted"]
detach = true
run = ["keepassxc", "{mountpoint}/pass.kdbx"]
A detached command is started in its own session and udevmon does not wait for it:
there is no timeout (setting one is an error), the actions after it and a
shutdown are not held up, its output is logged as usual, and its exit status is
logged when it ends. udevmon actions set NAME --detach or --no-detach changes it,
and actions list and explain show it.
Two limits. Under the systemd service, stopping the service stops everything the
actions started, detached or not (systemd ends the whole group); from a terminal they
keep running. To start something that must outlive the service, use
systemd-run --user --scope:
run = ["systemd-run", "--user", "--scope", "keepassxc"]
And a detached script runs entirely unattended, so it is not the place for work that must finish before something else starts.
Matching an unplug with its plug
An action for disconnected often needs something that the action for connected
found out: which YubiKey it was (the key itself reports no serial number, so a script
asks ykman), which folder it created, when it started. udevmon gives every plug, and
every mount, one value that is the same in all its events, and a private folder to keep
notes in:
| For | Set in | Same in | |
|---|---|---|---|
{instance} / $UDEVMON_INSTANCE |
one plug or mount | every event | connected, startup, disconnected, shutdown (USB); mounted, startup, unmounted, removed, shutdown (filesystem) |
{state_dir} / $UDEVMON_STATE_DIR |
the same, as a folder | every action | the same events |
$UDEVMON_STATE_ROOT |
every instance: the folder that holds the state folders | every action | while the daemon runs |
The instance is built on udev's USEC_INITIALIZED (different for every plug), or on
a counter when the device has none, and a new mount of a filesystem is a new instance.
It is the same across startup and the later removal of a thing that was already
there when the daemon started. It is unique only while the daemon runs.
The state folder is
$XDG_RUNTIME_DIR/udevmon/state/<instance> (private, mode 0700). It is created
before the first action of that instance runs, so a connect script can write into it,
and it is removed after the actions of its last event have finished: disconnected
for a USB device; unmounted for a filesystem, or removed when it was pulled (the
unmounted of a pull, with surprise=1, is followed by removed). The whole
state/ folder is wiped when the daemon starts and when it stops, so nothing outlives
a restart; startup is where a script rebuilds what it needs, as the old scripts did.
#!/bin/bash
# connected: remember which key this is
serial=$(ykman list -s | head -1)
echo "$serial" > "$UDEVMON_STATE_DIR/serial"
#!/bin/bash
# disconnected: the folder still has what connect wrote
serial=$(cat "$UDEVMON_STATE_DIR/serial")
Things to know: two plugged-in things have two folders, so several keys work at once.
The folder is only kept for events that carry an instance (everything udevmon sees
does). A detached action must not rely on it after the last event's actions have
finished, and an action of the connect event that is still running when the
disconnect's actions end may find it gone; give both the same lock to keep them apart.
A USB device and its filesystems each have their own instance; the only link
between them is surprise on disconnected.
Trying again (retries)
A command that can fail for a moment, such as unmounting right after a desktop has mounted and is still looking at a drive, can be run again:
udevmon actions add --device linuxsys mounted \
--retries 10 --retry-delay 1 -- udisksctl unmount -b "{devnode}"
[[actions]]
device = "linuxsys"
on = ["mounted"]
retries = 10 # up to 10 more attempts (default 0: none)
retry_delay = 1 # seconds before the first retry (default 1)
run = ["udisksctl", "unmount", "-b", "{devnode}"]
Unmounting what the desktop just mounted races with the desktop: a file
manager that opened a window for the new mount may mount it again when the
folder disappears, and this undoes mounts you make by hand too. If you only
want to keep a partition from being mounted automatically, a udev rule (needs
root) is cleaner, for example in /etc/udev/rules.d/99-no-automount.rules:
ENV{ID_FS_LABEL}=="LinuxSys", ENV{UDISKS_AUTO}="0"
When the command exits with a non-zero status it is run again after the delay,
up to retries more times, and each failed attempt is logged
(attempt 2 of 11 failed with status 1; retrying in 1 s). It is never retried after
a timeout, when the program cannot start, or when the action was skipped: repeating
those rarely helps. Each attempt has its own timeout, so the worst case is
(retries + 1) x timeout plus the delays.
With retry_backoff (a number, 1 or more; default 1) the waits grow: each is
that many times the one before, which suits a service that needs a while to
come back. retries = 5, retry_delay = 2 and retry_backoff = 2 wait 2, 4,
8 and 16 seconds.
Retrying stops at once when automation is paused or the daemon is stopping,
except for the shutdown event, whose retries go on while the daemon exits (the
service gets 120 seconds in all to stop). A lock is held across all the
attempts. retries, retry_delay and retry_backoff cannot be combined with
detach. udevmon actions set NAME --retries N --retry-delay S --retry-backoff F changes them (--retries 0 removes the retries,
--retry-backoff 1 the back-off).
Environment and starting folder (env, cwd)
An action can set environment variables of its own and the folder its command starts in (your home folder by default). Both may use placeholders:
[[actions]]
device = "backup-disk"
on = ["mounted"]
cwd = "{mountpoint}"
env = { LANG = "C", RESTIC_REPOSITORY = "{mountpoint}/restic" }
run = ["restic", "backup", "/home"]
actions add --env NAME=VALUE --cwd DIR sets them, and actions set has
also --unset-env NAME and --no-cwd. The UDEVMON_* names are udevmon's
own and cannot be set. A relative program path (./run.sh) counts from cwd,
and a folder that does not exist makes the action fail to start (the
failure output and notify_on_failure say so). explain shows both.
Being told when an action fails (notify_on_failure)
A failed action is normally only in the log. With notify_on_failure = true
(or actions add --notify-on-failure) the desktop shows a notification when
it exits with a status other than 0, times out or cannot start: the action and
device in the title, what happened in the text. With retries, only the last
attempt counts. It goes through the desktop session's D-Bus; without one
(no graphical login) nothing is shown and the log says so. It cannot be used
with detach. actions set NAME --no-notify-on-failure turns it off.
Running actions in a set order (order)
The actions of one event run one after another. By default that is file order,
but order (a whole number, default 50; lower runs first) decides it when the
order matters, whatever file each action is in. Equal ones keep file order.
A typical case is one tool wrapped around another: credentials published when a drive arrives must be there before the sync that needs them, and removed only after the final sync when it goes:
[[actions]]
name = "rclone-config-up"
tag = "rclone"
on = ["startup", "mounted"]
order = 10 # before the sync
run = ["rclone-config", "up", "{mountpoint}", "{drive_serial}:{fs_uuid}"]
[[actions]]
name = "rclone-config-down"
tag = "rclone"
on = ["unmounted", "shutdown"]
order = 90 # after the final sync
run = ["rclone-config", "down", "{mountpoint}", "{drive_serial}:{fs_uuid}"]
The sync's own actions keep the default 50. actions add --order N and
actions set NAME --order N set it; actions list shows the actions in the
order they run, and explain shows a non-default order.
Running actions one at a time (lock)
The actions of one event run one after another, but different events run at the same
time (up to max_workers commands at once). So the actions for a plug and for the
unplug that follows can overlap. Give the actions that must not overlap the same lock name:
udevmon actions add --tag yubikey connected,disconnected --lock yubikey -- \
yubikey.sh
udevmon actions set other-script --lock yubikey # or --no-lock
[[actions]]
tag = "yubikey"
on = ["connected", "disconnected"]
lock = "yubikey"
run = ["yubikey.sh"]
Actions that share a name never run at the same time, and they run in the order
their events arrived: a quick unplug cannot overtake its plug, even if a worker is
free. Actions without a lock, and locks with other names, are not affected, and
within one event the actions still run by order, then file order. A lock name
that only one action uses is legal (that action's own runs never overlap), and
udevmon check notes it, with the closest other name in case it is a typo. The lock is inside
udevmon: there is no lock file, and it does not protect against the same script being
run by hand (keep a flock in the script for that).
The lock is held while the command runs and is released as soon as the command has
exited (or, for detach, has started). A waiting action does not hold up
other events: it takes none of the max_workers slots until its command starts.
Every command has a timeout, so a stuck one holds the others with the same lock for
at most that long. The log says when an action
had to wait. What to use, by what the action does:
| The action | Use | The lock is held |
|---|---|---|
A short script (may leave a helper running with &) |
lock only |
Until the script exits |
Is the long-running program (keepassxc ...) |
detach = true |
Until it has started |
| Does critical work, then must start a long-running program | Two actions on the same event: the first with lock (the work), the second with detach (the launch), in that order |
The first until it exits; the second only for the start |
The third row works because the actions of one event run by order, then file
order: write the two in that order, with the same order (or none).
Command reference
udevmon scan [--present] [--no-prompt] [--all-props]
watch events and name things
udevmon present [--all] [--all-props] [--json]
list what is plugged in now
udevmon devices list | show NAME | remove NAME
udevmon devices identify MOUNTPOINT what udevmon knows of that drive
udevmon devices add NAME (--match KEY=VALUE ... | --from-mountpoint PATH)
[--tag TAG ...] [--exclude-tag TAG ...]
[--var NAME=VALUE ...] [--description TEXT]
[--file DROPIN]
udevmon devices tag NAME TAG... add explicit tags to a device
udevmon devices set NAME [--match KEY=VALUE ...] [--description TEXT]
[--var NAME=VALUE ...] [--unset-var NAME ...]
udevmon devices|tags|actions rename OLD NEW
udevmon devices disable | ignore | enable NAME...
switch devices off (see above)
udevmon devices untag NAME TAG...
udevmon devices exclude NAME TAG... opt a device out of rule tags
udevmon devices include NAME TAG... stop opting it out
udevmon tags list | show NAME | remove NAME
udevmon tags add NAME [--match KEY=VALUE ...] [--description TEXT]
[--file DROPIN]
udevmon tags set NAME [--match KEY=VALUE ... | --no-rule] [--var N=V ...]
[--description TEXT]
udevmon actions list | remove NAME
udevmon actions add (--device NAME ... | --tag NAME ...) EVENT[,EVENT...]
[--name NAME] [--timeout S] [--all-tags] [--detach]
[--lock NAME] [--retries N] [--retry-delay S]
[--retry-backoff F] [--order N] [--notify-on-failure]
[--allow-unnamed | --named-only] [--file DROPIN]
-- COMMAND...
udevmon actions set NAME [--allow-unnamed | --named-only
| --default-unnamed] [--on EVENTS] [--timeout S]
[--add-device NAME ...] [--remove-device NAME ...]
[--add-tag NAME ...] [--remove-tag NAME ...]
[--all-tags | --any-tags] [--detach | --no-detach]
[--lock NAME | --no-lock] [--retries N]
[--retry-delay S] [--retry-backoff F] [--order N]
[--notify-on-failure | --no-notify-on-failure]
[-- COMMAND...]
udevmon actions disable | enable NAME...
udevmon explain [EVENT] [--device NAME] [--prop KEY=VALUE ...] [--all]
[--json] what would run (see above)
udevmon pause [--for DURATION] stop every action (see above)
udevmon resume let actions run again
udevmon check [--json] validate the config
udevmon edit [FILE] [--no-reload] edit a file in $EDITOR, checked
udevmon apply [FILE] [--dry-run] [--no-reload]
several edits as one change (JSON)
udevmon version [--json] version and features
udevmon run [--dry-run] run the daemon in the foreground
udevmon reload make the running daemon reload
udevmon status [--json] the daemon; actions' last results
udevmon trigger DEVICE [EVENT] [--wait] [--json]
run its actions now (see above)
udevmon service install [--now] install the systemd user service
udevmon service uninstall stop and remove it
udevmon completion bash | zsh | fish | install | uninstall
[--shell SHELL] tab completion
man udevmon describes each command and option in full.
Global options, placed before the subcommand: --config PATH, -v (debug logging).
The editing commands (devices, tags, actions: add, remove, tag, untag)
also take --no-reload, and --dry-run, which shows the change as a diff and
writes nothing. devices list, devices show, tags list, tags show,
actions list, check, explain, present, trigger and version take
--json.
Manual device creation, without scanning:
udevmon devices add my-key --match vendor_id=1050 \
--match product_id=0407 --match serial=A1B2C3D4E5
A device can't be removed while an action lists it by name, and a declared tag ([tags.X])
can't be removed while an action uses it; remove those actions first. Repeating --match with
the same key on tags add builds a list (--match fs_type=ext4 --match fs_type=exfat);
on devices add a repeated key is an error, since a device match is exact.
A drive that is plugged in and mounted can be named from a script, without typing its identity:
udevmon devices add key-daily --from-mountpoint /media/$USER/KEYDRIVE \
--tag keepass
This matches the filesystem by fs_uuid and its drive by drive_serial, and
takes its description. Nothing is written when nothing is mounted there, the
filesystem has no UUID, the drive has no usable serial number, or a device
already matches it. udevmon present --json prints what is plugged in as JSON
(every property, the devices and tags each thing is, and the match scan
would suggest).
udevmon devices identify MOUNTPOINT shows the fs_uuid and
drive_serial of the drive mounted there, exactly as actions get them as
{fs_uuid} and {drive_serial}, and which devices match it; with --json
also the match --from-mountpoint would write, or why it cannot. A program
that keeps its own record of a drive can take its identity from here instead
of working it out again.
Actions created by hand without a name can't be removed through the CLI
(edit the file instead).
Examples
Notify when a specific USB stick is plugged in or removed:
udevmon actions add --device my-stick connected,disconnected -- \
notify-send "USB stick {event}"
Run a backup when the backup drive is mounted, and safely unmount afterwards:
[[actions]]
name = "backup"
device = "backup-disk"
on = ["mounted"]
run = ["sh", "-c",
'rsync -a ~/Documents/ "$1/Documents/" && udisksctl unmount -b "$2"',
"sh", "{mountpoint}", "{devnode}"]
timeout = 3600
Warn when a drive is pulled without ejecting it (its data may be damaged):
udevmon actions add --device backup-disk removed -- \
notify-send -u critical "Drive pulled without ejecting" "{fs_label}"
One action can handle both cases by looking at surprise. This logs a clean
eject quietly and warns loudly about a pull:
udevmon actions add --device backup-disk unmounted -- sh -c '
if [ "$1" = 1 ]; then
notify-send -u critical "Pulled without ejecting" "$2"
else
echo "$2 ejected cleanly" >> ~/eject.log
fi' sh {surprise} {fs_label}
Lock the screen when a security key is removed:
udevmon actions add --device my-key disconnected -- loginctl lock-session
Do something with a drive that is already connected when you log in and when it is plugged in later:
udevmon actions add --device backup-disk startup,mounted -- \
~/bin/sync-check.sh
Get a notification for any ext4/exfat drive that is mounted, and run the backup
only for the drives you named and tagged backup:
udevmon tags add storage --match fs_type=ext4 --match fs_type=exfat
udevmon actions add --tag storage mounted --allow-unnamed -- \
notify-send "Drive mounted: {tag}"
udevmon devices tag backup-disk backup
udevmon actions add --tag backup mounted --named-only -- \
~/bin/backup.sh "{mountpoint}"
Carry rclone credentials on a drive, available only while it is plugged in:
the helper in contrib/rclone-config/
publishes the drive's config/rclone/rclone.conf to
$XDG_RUNTIME_DIR/rclone/rclone.conf and removes it as soon as the drive is
gone. It's a separate small package that udevmon runs as an action, not
part of udevmon itself.
Security notes
Running commands automatically when hardware appears is convenient and also a classic attack vector ("BadUSB"). udevmon helps by:
- warning about weak matches when you create a device;
- preferring serial numbers and UUIDs (a plain vendor/product ID can be imitated by anyone);
- never running commands through a shell and never splicing device values into a command line;
- refusing to load a config file that is not owned by you or is writable by group/others, since that file decides what gets executed;
- running actions for hardware you haven't named only when the action says
allow_unnamed = true(see Named and unnamed things).
Keep scripts triggered by removable devices short, and don't run actions that trust the contents of the device without care.
Troubleshooting
- Nothing happens when I plug the device in. Run
udevmon -v runin a terminal and plug it in. Look fornothing matched, which means the properties differ from what you saved. Compare withudevmon presentorudevmon scan. Remember that a stick appears as both a USB device and a filesystem, and a device matches only one of those. - An action doesn't run when I expect it to. First check you haven't paused
automation (
udevmon explainsays so;udevmon resumeends it). Then runudevmon explain. It shows, for everything plugged in now, whether the thing isunnamedor a nameddevice:, whichtags:it has, and which actions would run or be skipped, with the reason. An unnamed thing runs only actions withallow_unnamed = true, and a thing with no tags matches no tag action at all.udevmon -v runlogs the same reasons, andudevmon checkexplains the notes aboutallow_unnamed. - The daemon says a command was not found. It checked with its own
PATH. Under systemd that is often shorter than your shell's, so put your scripts' folder inscript_dir, use the full path to the program inrun, or put the directory on the service'sPATH. - Filesystem events never fire. Check that udisks2 is running
(
systemctl status udisks2) and that the drive is mounted through it (for example by your desktop's automounter orudisksctl mount). Mounts done withsudo mountdo not go through udisks2 and are not seen. notify-sendor other graphical commands don't work from the service. udevmon finds your session'sDISPLAYby itself (see "Commands" above), and the log says which one it uses (journalctl --user -u udevmon, look for "graphical session for actions"). If it says "no graphical session found", no program of yours has a display, which happens when nobody is logged in to the desktop.- "refusing to load". The config file is group- or world-writable or owned
by someone else. Fix it with
chmod go-w ~/.config/udevmon/config.toml. - "udevmon is already running". Only one daemon runs per user. Stop the
service (
systemctl --user stop udevmon) before running it in a terminal. - The command isn't found. Services don't always see the same
PATHas your shell. Setscript_dir, or use full paths inrun.
Limitations
- Events are seen only while your user session is running and udevmon is
started; there is no replay of missed events (apart from
startup). - Only USB devices and udisks2 filesystems are supported out of the box. The
udev_filterssetting in[daemon](for example["usb/usb_device", "input"]) lets the daemon watch other kinds of udev devices, matched through theudev_<name>properties, but this is experimental and only the default USB filter has been tested. - Rule values are strings, lists of them, or patterns with
*and?(capitals ignored); there are no regular expressions. Variables belong to devices, not to tags. - No GUI or TUI yet; configuration is the TOML file plus the CLI.
Using udevmon from other programs
Other programs use udevmon through its command line, not by importing it. These are the interface, still provisional while udevmon is at 0.x (each change is in the changelog):
- the commands, their options and exit statuses;
- the configuration format, including drop-in files that a program owns
(
config.d/NN-NAME.toml); - what an action receives: placeholders, the
UDEVMON_*environment variables and the state folder; - the
--jsonoutput ofpresent,devices list|show|identify,tags list|show,actions list,check,explain,triggerandversion; every entry of the configuration says whichfiledefines it and, for a drop-in, itsdropinname (what--filetakes), and every finding ofcheck --json(infindings) the entries it concerns, so a program can pick out the ones about its own drop-in; udevmon --version: the version alone, on one line.
Rather than compare version numbers, test for a feature:
udevmon version --json lists names such as identify or edit-dry-run,
and a name is never reused. To see what an edit would do first, add
--dry-run: it prints a diff and writes nothing.
While udevmon is 0.x, any minor release may change this interface; each such
change is listed under Changed (incompatible) in the changelog, so pin a
range such as udevmon>=0.9,<0.10 and read that section before raising it.
A program's own tests can run udevmon apart from the user's: with
XDG_CONFIG_HOME and XDG_RUNTIME_DIR pointing at private folders, a
udevmon run uses its own configuration, lock and control socket, and
trigger, pause and reload given the same variables talk to it alone.
The Python modules, the control socket and the files in
$XDG_RUNTIME_DIR are internal and may change in any release. The
manual has the details, in "Interfaces
and stability" and "Integration contracts".
Development
How to run the tests and work on udevmon is in
DEVELOPMENT.md.
Real-hardware test
The unit tests use synthetic events. To check the real thing, including a stick pulled out while mounted, run the guided script with a USB stick you don't mind plugging in and out:
systemctl --user stop udevmon # only if you use the service
python3 tools/usb-test.py
It asks you to plug the stick in, pull it out without ejecting it (nothing
should be writing to it) and plug it in again, and does everything else. It
uses a throwaway config and runtime folder, switches off the desktop's "open a
window when a volume is mounted" setting while it runs and restores it, and
prints PASS or FAIL for each check. If one fails it writes
usb-test-report.txt in the current folder. --keep-popups leaves the
desktop setting alone.
SPECIFICATIONS.md
describes the design, data model and behaviour in detail, and
CHANGELOG.md
what changed in each version.
Metadata
Release files for udevmon 0.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| udevmon-0.10.0.tar.gz | 424.6 kB | With provenance | Details for udevmon-0.10.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| udevmon-0.10.0-py3-none-any.whl | Python 3 | none | any | Details for udevmon-0.10.0-py3-none-any.whl |
Total release size: 610.1 kB
Release files / udevmon-0.10.0.tar.gz
| Download URL | udevmon-0.10.0.tar.gz |
|---|---|
| Size | 424.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d3fc0290143dec57f1d87e3700639f222972e84fcd77d259a9d2ed495f9f74a8
|
|
BLAKE2b-256 checksum How to use checksums |
76b354829ef5a3797a06648b0fa12fa9d6bd2a7c9595e8d39f4de1a65a569708
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.
Transparency logRelease files / udevmon-0.10.0-py3-none-any.whl
| Download URL | udevmon-0.10.0-py3-none-any.whl |
|---|---|
| Size | 185.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6948027bd2c5801179016ab02158ae0b6a232b10474ff026d3f2bbee421465a4
|
|
BLAKE2b-256 checksum How to use checksums |
94b7efbe55e33927892b24242dd64873d7a2933cbf3e0328cdd403f031b33544
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.
Transparency logSecurity for udevmon 0.10.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for udevmon-0.10.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.