# gmailctl: Gmail filters as a Jsonnet file you can diff

> A Go CLI that keeps your Gmail filters in a declarative config, pushes them through the Gmail API instead of the settings screen, and tells you what changed before it changes anything.

**mbrt/gmailctl** — Declarative configuration for Gmail filters

- Repository: https://github.com/mbrt/gmailctl
- Stars: 2,205 · Forks: 87
- Language: Go
- License: MIT
- Published: 2026-10-08 · Updated: 2026-10-08 · Language: en
- Canonical page: https://hysenlabs.com/projects/mbrt-gmailctl

## Why filters end up in a file at all

The README opens with a story rather than a feature list, and the story is about a specific failure. You accumulate filters, one of them mislabels a message, and you scroll through a long list of rules trying to work out which one did it. The tooling you are handed at that moment is the Gmail settings screen, which has no version control, no find and replace across filters, and no way to try a change without applying it.

Gmail does let you export and import filters as XML, and the README is dismissive of that route on purpose. Most tools in this space stop there: they give you a small domain specific language that generates XML you import by hand. The README names antifuchs/gmail-britta as an example of that approach and rejects it.

The difference in gmailctl is that it uses the Gmail API, so applying filters is an automated step rather than a manual import. The project lists five goals, and they are worth reading in order because they explain the design: maintainability, a declarative and composable language, a built-in query simplifier, the ability to review changes before applying them, and updates that land in seconds rather than minutes.

## The 1500 character limit is a design constraint, not an afterthought

Gmail caps each filter query at 1500 characters. The README lists a built-in query simplifier among the project's goals specifically to keep filter size down against that limit, which tells you the author hit the ceiling in real use rather than reading about it.

The configuration is Jsonnet, a superset of JSON with local variables, composition and comments. That choice does real work here. Filters that share a sender list can be written once and referenced, so tightening a rule later means editing one line rather than hunting through every filter that mentions that domain. The repository ships `example.jsonnet` at the root as a starting point, and the topics list jsonnet, gmail, filters and labels.

```bash
go install github.com/mbrt/gmailctl/cmd/gmailctl@latest
```

Prebuilt binaries for Linux, macOS and Windows on amd64 and arm64 are published on the releases page, and the README also documents Homebrew, MacPorts, Fedora's dnf and snap. For snap users it suggests setting `xdg-mime` so the config file opens in your editor of choice.

## diff and edit are the two commands you will actually run

The command list in the README is short: apply, debug, diff, download and edit. `gmailctl edit` opens the config in your editor and applies it to Gmail when you exit, which the README calls the preferred way to start from scratch. `gmailctl diff` shows the difference between your local config and what Gmail currently holds. `gmailctl apply` pushes without opening an editor, which is the one you want in a script.

The review step is not a nicety here, because the first run is destructive. The README is explicit about it: your current filters will be wiped and replaced by the ones in the config, so back up before the first run, and expect the first diff to be large. After that the diffs are meant to be small and easy to read, which is the whole point of the tool.

```bash
# download the filters to the default configuration file
mkdir -p ~/.config/gmailctl
gmailctl download > ~/.config/gmailctl/config.jsonnet
# check that the diff is empty and no errors are present
gmailctl diff
# happy editing!
gmailctl edit
```

That block is the README's own migration recipe, and it happens to be the correct procedure whether you are coming from hand-built XML filters or from another tool.

## Downloaded filters arrive escaped, and you have to port them by hand

The migration path has an honest limitation, and the README states it directly. The `download` command looks up your existing filters in Gmail and tries to build a config that matches, but it is labelled experimental, the README advises checking the result against the remote filters before making changes, and it warns the file will be quite ugly because expressions are not reconstructed properly.

The specific artefact to look for is the `isEscaped: true` marker. It tells gmailctl to leave the expression alone instead of quoting or escaping it, because the original contained operators that Gmail has to interpret as they are. It appears wherever `download` could not map a filter onto native gmailctl expressions:

```jsonnet
{
  from: "{foo bar baz}",
  isEscaped: true,
}
```

The README then shows the manual conversion into real operators:

```jsonnet
{
  or: [
    {from: "foo"},
    {from: "bar"},
    {from: "baz"},
  ],
}
```

So the practical reading of the download feature is that it gives you a starting file, not a finished one. Port the escaped entries before you rely on it.

## Configuration location has three rules and the newest one is in the release notes

Where the config lives is documented in three layers, and reading them in order explains some surprises. The current default follows the XDG base directory spec: `$XDG_CONFIG_HOME/gmailctl`, which is `~/.config/gmailctl` on Linux when the variable is unset, and `~/Library/Application Support/gmailctl` on macOS. Run `gmailctl --help` to see the exact path on your system.

The second layer is `~/.gmailctl`, the previous default. If that directory exists, gmailctl keeps using it for backward compatibility, which means an old install silently keeps its old config location. The third is the `--config` argument for anything custom.

Here is a place where the README and the release history are not quite aligned. The README presents the XDG path as the default you get, and the v0.13.0 release notes announce a change to default newly created config directories to `~/.config/gmailctl`. Those statements only fully agree on Linux, since the README's macOS default is the Application Support directory. If you have a Mac and care where the file appears, check `gmailctl --help` on your machine rather than assuming either one. The fallback to `~/.gmailctl` is the reason the ambiguity is harmless in practice.

## OAuth setup and a repository that keeps its own agent instructions

Authorisation is a two step process, and it requires a trip to the Google Auth Platform console. `gmailctl init` walks you through enabling the Gmail API and registering a Desktop app OAuth client. You save the downloaded client JSON at the path the tool prints, then run the same `gmailctl init` again to complete authorisation. Credentials live in the config directory alongside the config file.

The rest of the repository is organised like a Go project that expects to be worked on. There is a `cmd/` directory, an `internal/` directory, `integration_test.go` at the root, `testdata/`, a `hack/` directory, and a `snap/` directory for packaging. Quality tooling is visible too: `codecov.yml`, `revive.toml` for linting, `renovate.json` for dependency updates, and `.goreleaser.yml` for the release artifacts across those four install paths.

There is an `AGENTS.md` at the root, which tells you this repository is set up for automated contributors. The test layout is the part worth checking before you report a bug: unit tests live alongside the packages in `internal/`, and the root-level integration test exercises the CLI end to end. Both exist, so a failing filter is more likely a configuration problem than a missing test harness.

Latest release v0.13.0 shipped on 2026-09-28 with a breaking change worth noting if you script against this tool: it now rejects quotes that cannot produce a valid Gmail query, rather than accepting them and failing later at Gmail. That is a change that protects you from silent breakage, but it can surface errors in a config that used to apply.

## Conclusion

gmailctl earns its place on the strength of one workflow: write filters in a file, run `gmailctl diff`, read the change, then run `gmailctl edit` to push it. That loop only works after you have a config that matches reality, and the fastest route to one is `gmailctl download`, whose own documentation calls the output ugly and experimental because expressions are not reconstructed. Expect to hand port anything marked `isEscaped: true` before you trust it with a live inbox. If you would rather keep filters in XML, the README names the alternative approach and explains why this one exists.

## FAQ

### How do I run my Gmail filters?

With gmailctl you keep filters in a Jsonnet file and let the CLI push them through the Gmail API. gmailctl diff shows what differs between your file and what Gmail currently holds, and gmailctl edit opens the file in your editor and applies it on exit. The first run replaces your existing filters, so back them up before you start.

### Does gmailctl export my existing Gmail filters?

There is a download command for that, and the README calls it experimental. It fetches your current filters and writes a Jsonnet config that roughly matches them, but expressions are not reconstructed properly so the output is ugly, and some entries arrive with an isEscaped: true marker that means the expression is passed through unquoted. The advice is to check the generated file against the live filters before changing anything.

### Which platforms can I install gmailctl on?

Prebuilt binaries cover Linux, macOS and Windows on both amd64 and arm64 from the GitHub releases page. The README also documents Homebrew and Macports on macOS, the official Fedora dnf repositories, and a snap package. If you have Go installed you can also build from source with go install.

### Where does gmailctl keep its configuration file?

By default it follows the XDG base directory spec, so the config lives in $XDG_CONFIG_HOME/gmailctl, which resolves to ~/.config/gmailctl on Linux and to ~/Library/Application Support/gmailctl on macOS. An older ~/.gmailctl directory is still honoured if it exists, for backward compatibility, and the --config argument overrides both. Run gmailctl --help to see the exact path on your machine.

## Sources

- [Issues](https://github.com/mbrt/gmailctl/issues)
- [License: MIT](https://github.com/mbrt/gmailctl/blob/master/LICENSE)
- [mbrt/gmailctl on GitHub](https://github.com/mbrt/gmailctl)
- [README](https://github.com/mbrt/gmailctl/blob/master/README.md)
- [Releases](https://github.com/mbrt/gmailctl/releases)

---

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