bug#81531: 30.2; filenotify: moving file out of watched directory delays the "deleted" event until file is later moved back in

Michael Albinus via "Bug reports for GNU Emacs, the Swiss army knife of text editors" <[email protected]> Tue, 04 Aug 2026 16:14:07 +0200
Newsgroups gmane.emacs.bugs
Message-ID <[email protected]>
i_ix_iv <[email protected]> writes:

Hi,

> Using a basic filenotify watcher on a directory and testing several
> file movement commands doesn't result in expected `delete` actions being
> caught by the filenotify watcher.

Thanks for the report. However, this isn't a bug.

Before we're going to analysis, just some few words about
filenotify.el. It is an umbrella for different file notification
backends, like inotify, kqueue, gio, w32notify, and several file name
handlers for remote files/directories. All of these backends raise
different events, depending on the underlying OS. filenotify.el
transforms them into the generic events documented in
file-notify-add-watch. In your case, the inotify backend is in use.

> Starting from `emacs -Q`:
>  - Run the below ELisp:
>
> ```elisp
> (progn
>   (require 'filenotify)
>   (setq my-watch
>     (file-notify-add-watch
>       default-directory
>       '(change)
>       (lambda (event)
>         (message "%S" event)))))
> ```

We must investigate both events coming from the respective backend, and
the generic events you'll see in your handler. For tests, I use the
following call:

--8<---------------cut here---------------start------------->8---
# ~/src/emacs-30/src/emacs -Q -l filenotify --eval "(setq file-notify-debug t)" --eval "(file-notify-add-watch default-directory '(change) #'ignore)"
--8<---------------cut here---------------end--------------->8---

Setting `file-notify-debug' to t enables all needed traces, so we can use
the `ignore' handler.

>     1 -> `touch foo`

--8<---------------cut here---------------start------------->8---
file-notify-handle-event (file-notify ((1 . 0) (create) "foo" 0) file-notify--callback-inotify)
file-notify-callback (1 . 0) created "/home/albinus/.emacs.d/foo" nil #s(file-notify--watch "/home/albinus/.emacs.d" nil ignore) "/home/albinus/.emacs.d" "/home/albinus/.emacs.d"
--8<---------------cut here---------------end--------------->8---

inotify sends the 'create "foo"' event. This is transformed into
'created "/home/albinus/.emacs.d/foo"', which is shown to the
handler. Good.

>     2 -> `mv foo bar`

--8<---------------cut here---------------start------------->8---
file-notify-handle-event (file-notify ((1 . 0) (moved-from) "foo" 357539) file-notify--callback-inotify)
file-notify-handle-event (file-notify ((1 . 0) (moved-to) "bar" 357539) file-notify--callback-inotify)
file-notify-callback (1 . 0) renamed "/home/albinus/.emacs.d/foo" "/home/albinus/.emacs.d/bar" #s(file-notify--watch "/home/albinus/.emacs.d" nil ignore) "/home/albinus/.emacs.d" "/home/albinus/.emacs.d"
--8<---------------cut here---------------end--------------->8---

We get two events from inotify: 'moved-from "foo"', and 'moved-to
"bar"'. Since both events carry the same cookie (357539), they belong
together. Therefore, they are transformed into the generic event
'renamed "/home/albinus/.emacs.d/foo"
"/home/albinus/.emacs.d/bar"'. Still good.

>     3 -> `mv bar ..`

--8<---------------cut here---------------start------------->8---
file-notify-handle-event (file-notify ((1 . 0) (moved-from) "bar" 370367) file-notify--callback-inotify)
--8<---------------cut here---------------end--------------->8---

This is the problematic action. We get from inotify only a 'moved-from
"bar"' event. The corresponding 'moved-to' event is not returned,
because the file path is ouside of the watched directory.

filenotify.el is instructed, to wait for the next backend event after
'moved-from'. This is needed to decide what to do. Since no second
backend event arrives, nothing is sent to the handler. This is what you
have observed.

>     4 -> `mv ../bar .`

--8<---------------cut here---------------start------------->8---
file-notify-handle-event (file-notify ((1 . 0) (moved-to) "bar" 389895) file-notify--callback-inotify)
file-notify-callback (1 . 0) created "/home/albinus/.emacs.d/bar" nil #s(file-notify--watch "/home/albinus/.emacs.d" nil ignore) "/home/albinus/.emacs.d" "/home/albinus/.emacs.d"
--8<---------------cut here---------------end--------------->8---

inotify raises two events 'moved-from' and 'moved-to', again. But we see
the latter only, because the file path of the 'moved-from' event is not
seen.

Now, inotify.el has two events 'moved-from' "bar"' (from step 3) and
'moved-to "bar"' (from step 4) in order. However, the cookies are
different (370367 and 389895), so these events do *not* belong to each
other. Therefore, filenotify.el decides, that the first event has to be
understood as 'deleted' (not shown in the trace, I'll fix it), and the
second event has to be understood as 'created'. Both generic events are
forwarded to the handler.

>     5 -> `rm bar`

--8<---------------cut here---------------start------------->8---
file-notify-handle-event (file-notify ((1 . 0) (delete) "bar" 0) file-notify--callback-inotify)
file-notify-callback (1 . 0) deleted "/home/albinus/.emacs.d/bar" nil #s(file-notify--watch "/home/albinus/.emacs.d" nil ignore) "/home/albinus/.emacs.d" "/home/albinus/.emacs.d"
--8<---------------cut here---------------end--------------->8---

inotify event 'delete "bar"', which is forwarded to the handler as
'deleted "/home/albinus/.emacs.d/bar"'. Good.

I hope this clarifies what happened.