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:
./.apex/plugins (current working directory)
BASE/.apex/plugins when you run with --base-dir BASE
<git-root>/.apex/plugins when you’re inside a Git work tree
- 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:
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:
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:
- Global config
$XDG_CONFIG_HOME/apex/config.yml, or
~/.config/apex/config.yml
- 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
- Explicit metadata file
- Any file you pass with
--meta-file FILE
- Per-document metadata
- YAML front matter, MultiMarkdown metadata, or Pandoc title blocks
- 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.