If you use Apex for more than one project, you’ve probably hit the point where a single global setup doesn’t quite cut it. Maybe your book project wants a different plugin set than your docs site, or one repo has stricter defaults than everything else on your machine.

As a personal example, most of my app documentation has always been written in MultiMarkdown, where header ids get generated with no spaces or dashes, so all of my cross-references link to #thissection type of anchors. My blog and my Jekyll-based documentation sites have always used Kramdown, so header ids and cross references are #this-section. I needed an easy way to always have Apex use the right header format for the current project.

I also have special plugins for different destinations. For example, my Marked documentation has special Liquid-style tags like prefpane that generates nice HTML for referencing Preference panes with x-marked URLs that will open a preference pane directly in Marked. I don’t need or want a plugin to do that universally, the output it generates is very specific to Marked.

So I added project-scoped plugins and configurations to Apex. This allows me to get the settings just right for a project, then save them into a local directory and be able to just run apex without a bunch of command line flags to remember.

You also get a cleaner way to “shadow” plugins you don’t want with a local noop plugin.

I also added ++insert++ syntax for adding <ins>insert</ins> tags, but that’s just a little one-off addition.

Project-scoped plugins in .apex/plugins

Plugins used to be purely global: Apex would only look in your XDG config dir:

  • $XDG_CONFIG_HOME/apex/plugins, or
  • ~/.config/apex/plugins

Now there’s a proper project scope, searched in this order:

  1. ./.apex/plugins (current working directory)
  2. BASE/.apex/plugins when you run with --base-dir BASE
  3. <git-root>/.apex/plugins when you’re inside a Git work tree
  4. Global: $XDG_CONFIG_HOME/apex/plugins or ~/.config/apex/plugins

Each of those directories can hold Apex plugins in the usual format:

.apex/
  plugins/
    my-plugin/
      plugin.yml
      whatever-script-you-like

When Apex builds the plugin list, earlier locations win by id. If a plugin with id footnotes-plus exists in both .apex/plugins and your global config dir, the project version is the one that runs.

No-op shadowing: turning off plugins per project

That id-based precedence also gives you a neat trick: no-op shadowing.

If there’s a global plugin you usually like, but you don’t want it in a specific project, you can “shadow” it by dropping an empty or no-op plugin with the same id into .apex/plugins. For example:

.apex/
  plugins/
    kbd/
      plugin.yml
      noop.sh

plugin.yml might look like:

id: kbd
title: KBD Noop

Because the project copy of kbd is discovered first, it shadows the global one. You can also do the same trick with purely declarative regex plugins: define a plugin with the same id that simply doesn’t match anything meaningful, and the global behavior is effectively disabled for that project.

--list-plugins now understands projects

To make this discoverable, apex --list-plugins was updated to use the same resolution rules as the runtime plugin loader.

When you run:

apex --list-plugins

you’ll see:

  • An “Installed Plugins” section that includes plugins from:
    • ./.apex/plugins
    • BASE/.apex/plugins (if --base-dir was used)
    • <git-root>/.apex/plugins
    • global config plugins
  • An “Available Plugins” section from the remote directory, filtered so remote entries are hidden when you already have a plugin with the same id installed anywhere (project or global).

If a project plugin shadows a global one, you’ll only see the project entry in the installed list, and the remote listing won’t try to “helpfully” re-offer the same id.

Project-level config in .apex/config.yml

Plugins aren’t the only thing that benefit from scoping. Apex’s configuration system now has an explicit project layer, alongside the existing global and per-document metadata.

Config is now read from these places:

  1. Global config
    • $XDG_CONFIG_HOME/apex/config.yml, or
    • ~/.config/apex/config.yml
  2. Project config
    • ./.apex/config.yml
    • BASE/.apex/config.yml when using --base-dir BASE
    • <git-root>/.apex/config.yml when inside a Git work tree
  3. Explicit metadata file
    • Any file you pass with --meta-file FILE
  4. Per-document metadata
    • YAML front matter, MultiMarkdown metadata, or Pandoc title blocks
  5. Command-line metadata and flags
    • --meta KEY=VALUE, --mode, --pretty, --no-tables, and so on

The merge order matters:

  • Global config.yml (lowest file precedence)
  • Project .apex/config.yml
  • --meta-file FILE
  • Document metadata
  • --meta and CLI flags (highest precedence)

So if you put this in your global config:

mode: unified
pretty: true
wikilinks: false

and then in your project .apex/config.yml:

wikilinks: true
indices: true

you’ll end up with:

  • mode: unified
  • pretty: true
  • wikilinks: true # project overrides global
  • indices: true # project-only addition

Any --meta-file you pass on the command line layers on top of both, and document/CLI overrides still win last.

A quick example project layout

Here’s what a repo might look like with all of this wired up:

my-book/
  .git/
  .apex/
    config.yml
    plugins/
      figures/
        plugin.yml
        figures.py
      kbd/
        plugin.yml
  chapters/
    01-intro.md
    02-deep-dive.md

Running:

cd my-book
apex chapters/01-intro.md --plugins

will:

  • Load config from:
    • global config.yml (if any),
    • then ./.apex/config.yml,
    • then any --meta-file you pass,
  • Run plugins from:
    • ./.apex/plugins first,
    • then fall back to global plugins,
  • Apply per-document metadata and CLI overrides on top.

You get per-repo behavior without having to constantly remember the right --meta-file or a long list of flags.

A quick note on ++insert++

While we were in here, another small but handy syntax has been added: ++insert++.

++insert++ gives you a lightweight way to add an <ins>text</ins> tag to your document. It’s just a little shorter and easier than typing out the tags manually. I try not to add too much esoteric markup to the syntax, but I’ve seen ++ a couple of places and thought it a worthwhil addition.

Wrapping up

To recap:

  • Plugins can now live in .apex/plugins at the project level, and they override global plugins by id.
  • --list-plugins shows the actual set of plugins Apex will run for your current project, including overrides.
  • Config can now live in .apex/config.yml, layered on top of your global config.yml and below any explicit --meta-file, document metadata, and flags.
  • ++insertion++ gives you <ins> tags.

If you’ve been juggling different shell aliases or wrapper scripts for each project, you can probably simplify a lot of that now by letting Apex’s own project-aware behavior do the heavy lifting.