# listen: file watching for Ruby, with an honest list of what it will not do

> The listen gem watches directories through OS-specific adapters and reports modified, added and removed paths in a callback, with ignore, only and latency options. Its README is unusually candid: symlinks are always followed, TCP support was removed in 3.0.0, and listeners do not notify across forked processes.

**guard/listen** — The Listen gem listens to file modifications and notifies you about the changes.

- Repository: https://github.com/guard/listen
- Website: https://rubygems.org/gems/listen
- Stars: 1,959 · Forks: 254
- Language: Ruby
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/guard-listen

## Three arrays, one callback

The API is small and the callback shape is the whole contract.

```ruby
require 'listen'

listener = Listen.to('/srv/app') do |modified, added, removed|
  puts(modified: modified, added: added, removed: removed)
end
listener.start
sleep
```

The callback receives three array parameters, `modified`, `added` and `removed`, in that order. Each is always an array with zero or more entries, and each entry is an absolute path. There is no single unified change list, so if your handler does not care about the difference between a create and an edit you have to merge the arrays yourself.

The README's worked example makes the semantics concrete. A `touch a.txt` reports added with the new path. Appending to it reports modified. A `mv a.txt b.txt` reports both added and removed, which is the case that breaks naive handlers that assume one event per change. Removing two files reports both paths in a single removed array.

`listener.start` starts a listener thread and does not block, which is why the example ends with `sleep` and the comment about not exiting the process.

## pause, processing and the queue you did not know about

The lifecycle methods have a subtlety that matters more than it first appears, because pausing does not discard anything.

```ruby
listener.start
listener.paused?     # => false
listener.processing? # => true

listener.pause       # stops processing changes (but keeps on collecting them)
listener.paused?     # => true
listener.processing? # => false

listener.start       # resumes processing changes
listener.stop        # stop both listening to changes and processing them
```

While paused, listen keeps collecting changes in the background. To clear them, you have to call stop rather than start. So a paused listener that resumes will hand you a backlog of everything that happened while you were away, and if you were pausing to avoid a burst of work you will get the burst on resume.

The two predicates are also distinct. `paused?` describes whether you asked it to stop processing, while `processing?` describes whether it is currently doing so.

The README also states plainly that you should keep track of all started listeners and stop them properly on finish, which is ordinary resource hygiene but easy to skip when a watcher is created inside a library.

## ignore, ignore! and only differ in ways that bite

Path filtering has three forms and the difference between them is not cosmetic.

```ruby
listener = Listen.to('dir/path/to/listen', ignore: /\.txt/) { |modified, added, removed| # ... }
listener.start
listener.ignore! /\.pkg/ # overwrite all patterns and only ignore pkg extension.
listener.ignore /\.rb/   # ignore rb extension in addition of pkg.
sleep
```

`ignore` adds patterns to the defaults, while `ignore!` overwrites them entirely, so calling `ignore!` first with a narrow pattern and then `ignore` with another is how you build a custom set. Without `ignore!` you are inheriting the defaults, which are defined in `Listen::Silencer` as `DEFAULT_IGNORED_FILES` and `DEFAULT_IGNORED_EXTENSIONS`.

Those defaults matter because an editor writing a swap file or a temporary file under your watched directory produces events you did not ask for, and the default set is what filters them.

`only` is the inverse filter, and it is not a companion to `ignore`; it is a separate mode.

```ruby
listener = Listen.to('dir/path/to/listen', only: /\.rb$/) { |modified, added, removed| # ... }
listener.start
listener.only /_spec\.rb$/ # overwrite all existing only patterns.
sleep
```

There are two path rules to remember. `:ignore` regexp patterns are evaluated against relative paths, and `:only` regexp patterns are evaluated only against relative file paths. That difference is easy to miss and produces a pattern that matches nothing.

Filtering by regexp is also listed among the features as a way to get more accuracy and speed, since a pattern that excludes a directory tree stops the adapter descending into it.

## The limitations list is the most useful section

Most gems put their caveats at the end. This one puts them near the top, and they are specific.

Symlinked directories are limited. Symlinks are always followed, and symlinked directories pointing within a watched directory are not supported. If your project uses a symlinked source tree, that is where your events go missing.

TCP functionality was removed in 3.0.0. There are plans to extract it into separate gems, and until that is finished the workaround is to lock the listen gem to version `'~> 2.10'`. So the feature you may have used in 2.x is gone from 3.x and the escape hatch is to stay on an old major version.

Some filesystems will not work without polling: VM and Vagrant shared folders, NFS, Samba and sshfs are named explicitly. That is the single most common way this gem fails for someone new, and it fails by not notifying rather than by erroring.

The BSD adapter is not continuously and automatically tested, and the OSX adapter has performance limitations.

The one that surprises people in production is the last: listeners do not notify across forked processes. If you want multiple processes to receive change notifications, you must listen inside of each process. A forking server that watches in the parent and expects workers to see events will see nothing.

There is also no directory or adapter specific configuration, and plugin support is planned for the future rather than available.

## Adapters are the reason this gem exists at all

The feature list leads with OS-optimized adapters on MRI for Mac OS X 10.6 and later, Linux, BSD and Windows, and the point is the performance consequence of using native notification rather than polling.

Latency is the option that connects the two modes. `latency: 0.5` sets the delay in seconds between checking for changes, with a default of 0.25 seconds, or 1.0 second when polling. The polling default is 1.0 second.

There is a separate note about increased change detection accuracy on OS X HFS and VFAT volumes, which is the same story from the other direction: those filesystems report events in ways that need handling.

The repository itself is a conventional gem layout, with listen.gemspec, Rakefile, Gemfile, Guardfile,.rspec and.rubocop.yml at the root, lib/, spec/, bin/ and a CHANGELOG.md. Version 3.10.1 was released on 2026-10-01 after v3.10.0 on 2026-01-16, and the last push was on 2026-10-01, so releases and commits are landing together.

## Conclusion

Do not adopt it on NFS, Samba, sshfs or a Vagrant shared folder without switching to polling, and do not rely on it to notify child processes, since listeners do not cross a fork and each process must listen for itself. Before you write your adapter code, check whether your paths are symlinks, since they are followed unconditionally and symlinked directories inside a watched directory are unsupported.

## FAQ

### How do I add the listen gem to a Ruby project?

With Bundler, by adding `gem 'listen'` to your Gemfile. The gem is published on rubygems.org as listen, and version 3.10.1 is the most recent release.

### What does the listen callback receive?

Three array parameters in order: modified, added and removed. Each is always an array with zero or more entries and each entry is an absolute path. A rename reports the new path in added and the old path in removed in the same callback.

### Does listen work on NFS or Vagrant shared folders?

Not without polling. The README names VM and Vagrant shared folders, NFS, Samba and sshfs as filesystems that will not work without polling, since the adapters rely on native OS change notification.

### What is the difference between ignore and ignore! in listen?

ignore adds patterns to the default ignored set defined in Listen::Silencer, while ignore! overwrites the defaults entirely. The only option works the same way but filters for the files you want instead of against the ones you do not.

### Can listen send notifications to forked processes?

No. The README states that listeners do not notify across forked processes, and that if you want multiple processes to receive change notifications you must listen inside each process.

## Sources

- [guard/listen on GitHub](https://github.com/guard/listen)
- [License: MIT](https://github.com/guard/listen/blob/master/LICENSE)
- [Project website](https://rubygems.org/gems/listen)
- [README](https://github.com/guard/listen/blob/master/README.md)
- [Releases](https://github.com/guard/listen/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/guard-listen
