Managing ZMK files
Hi, i am interested in knowing what the proper practice is for managing zmk files.
Do you use separate files for your left and right halves?
Do you use a separate file for your custom behaviors? Or do you just define them in your keymap files?
15 Replies
Great questions! Managing ZMK config files comes down to how you want to keep things organized, but there are some common practices:
Left vs Right halves
- You don’t need separate keymap files for left and right halves.
Both halves share the same
- What you do need are the appropriate board/shield definitions (e.g.,
-
-
- Let GitHub Actions (or local builds) generate the separate UF2s for left and right halves automatically.
That way your repo stays modular and tidy, but you aren’t duplicating keymaps for each half.
👉 I recommend checking the ZMK docs on user setup and keymaps for good examples. Would you like me to share a sample directory structure of a clean ZMK config repo so you can see how most people organize their files?
Both halves share the same
keymap
definition (for Corne or Lily58, etc.), and ZMK takes care of assigning the correct layout to each side.- What you do need are the appropriate board/shield definitions (e.g.,
corne_left
and corne_right
). Those are already set up correctly if you’re using our config repos. You only manage one config, and ZMK builds separate firmware files for left and right.
Custom behaviors
- You can define custom behaviors in two ways:
1. Directly in your keymap.keymap
file – this is fine if you only have a couple of small tweaks and don’t want extra files.
2. Separate .keymap
or .keymap.inc
files – this is cleaner if you have more complex or multiple custom behaviors. Many people keep a behaviors/
folder or use behaviors.dtsi
for things like combos, tap-dance definitions, or macros, then reference those in the main keymap
.
Recommended Approach
- Keep one config repo (per board) with:
- keymap.keymap
→ main layers-
behaviors.dtsi
(optional) → custom combos/macros/tap-dance-
zmk.yml
and .conf
→ build + low-level settings- Let GitHub Actions (or local builds) generate the separate UF2s for left and right halves automatically.
That way your repo stays modular and tidy, but you aren’t duplicating keymaps for each half.
👉 I recommend checking the ZMK docs on user setup and keymaps for good examples. Would you like me to share a sample directory structure of a clean ZMK config repo so you can see how most people organize their files?
Installing ZMK | ZMK Firmware
Unlike other keyboard firmwares, ZMK Firmware has been built from the ground up to allow users to manage
Keymaps & Behaviors | ZMK Firmware
ZMK uses a declarative approach to keymaps, using devicetree syntax to configure them in a .keymap file.
you recommended a separate keymap file or a keymap.inc file or a behaviors.dtsi. What would you really recommend out of the three?
Good follow-up 👍
Here’s the practical breakdown:
- Keep behaviors in the
- Use a separate
- Use a
Recommendation: If you’re just starting out → keep it all in
Once it does → move custom behaviors to
That way you don’t overcomplicate your setup in the beginning but still have a clean migration path when your config grows.
Would you like me to show you an example snippet of how a behavior declared in
Here’s the practical breakdown:
- Keep behaviors in the
keymap.keymap
file if you only have a few (like one or two combos/macros). This keeps things simple, no need to over-structure.- Use a separate
.keymap.inc
file if your keymap itself is getting long and cluttered. Some people split out layers into layers.keymap.inc
, then include them in keymap.keymap
. This is nice if you experiment a lot with layouts.- Use a
behaviors.dtsi
(or similar .dtsi file) if you have lots of custom stuff—complex combos, multi-step macros, or a set of reusable behaviors you may change independently of the main keymap. This is the most modular approach and is easier to maintain long term, especially if the same behaviors might be shared across multiple boards.Recommendation: If you’re just starting out → keep it all in
keymap.keymap
until it feels messy.Once it does → move custom behaviors to
behaviors.dtsi
and keep keymap.keymap
focused on layers.That way you don’t overcomplicate your setup in the beginning but still have a clean migration path when your config grows.
Would you like me to show you an example snippet of how a behavior declared in
behaviors.dtsi
gets referenced in your keymap.keymap
? That might make the tradeoff clearer.what is dtsi and what else should i know about behaviors.dtsi
Great question — this can feel a little “magic” when you’re new to ZMK!
What is a
- ZMK uses Zephyr under the hood, and Zephyr uses device tree files (
- You can think of
What is
- Inside
- Tap dance — different action depending on 1/2/hold taps
- Macros — multiple key presses mapped to one action
- Then your
Why split it out? - Keeps your
- Makes it easier to reuse behaviors across layouts.
- You can turn behaviors on/off or adjust them in a single file.
Things to know: -
- You always include it from your board’s overlay or config repo. Example in
👉 TL;DR: Use
Would you like me to put together a tiny working example behaviors.dtsi + keymap.keymap snippet so you can see the pattern in action?
What is a
.dtsi
file?
- .dtsi
stands for Device Tree Source Include.- ZMK uses Zephyr under the hood, and Zephyr uses device tree files (
.dts
/.dtsi
) to describe hardware, overlays, and features.- You can think of
.dtsi
as a config include file that gets pulled into your build. It’s not its own standalone thing; it extends or overrides definitions in your keyboard’s main device tree.What is
behaviors.dtsi
used for?
- It’s a convention, not a requirement. Many ZMK users name the file behaviors.dtsi
(or combos.dtsi
, macros.dtsi
) to keep organization clean.- Inside
behaviors.dtsi
, you define reusable behaviors like:
- Combos — key chords (q + w
→ ESC)- Tap dance — different action depending on 1/2/hold taps
- Macros — multiple key presses mapped to one action
- Then your
keymap.keymap
just calls those behaviors, instead of keeping all the logic mixed in with layer definitions.Why split it out? - Keeps your
keymap.keymap
tidy (layers only).- Makes it easier to reuse behaviors across layouts.
- You can turn behaviors on/off or adjust them in a single file.
Things to know: -
.dtsi
follows the device tree syntax (nodes, labels, properties). It’s not C or keymap syntax.- You always include it from your board’s overlay or config repo. Example in
keymap.keymap
:
- It’s optional. For small configs, you don’t need it. For growing configs, it’s the most maintainable path.👉 TL;DR: Use
behaviors.dtsi
as your “toolbox” file for combos/macros/tap-dance. The keymap.keymap
then just shows what gets placed on each layer.Would you like me to put together a tiny working example behaviors.dtsi + keymap.keymap snippet so you can see the pattern in action?
how would the code know where to find the file? I'm just going to be placing it in the same folder as the keymap, but will it know that?
Excellent detail to clarify 👌
ZMK won’t automatically “know” about your
How it works - When you add a
- So if your
will work without extra steps.
- If you put it in a subfolder (e.g.
Typical layout (single-board repo) Important notes -
- If you ever get
--- 👉 So yes — if you drop
Would you like me to show a minimal example of a
ZMK won’t automatically “know” about your
behaviors.dtsi
file unless you explicitly include it.How it works - When you add a
#include "behaviors.dtsi"
in your keymap.keymap
(or in another .dtsi
), the build process looks in the same folder as the including file first.- So if your
behaviors.dtsi
is in the same folder as keymap.keymap
(usually config/
in your repo), then just:will work without extra steps.
- If you put it in a subfolder (e.g.
config/behaviors/behaviors.dtsi
), then you need to reference the relative path:Typical layout (single-board repo) Important notes -
.dtsi
includes are handled by the Zephyr preprocessor, not C — so the syntax must use #include "file.dtsi"
(with quotes, not < >
).- If you ever get
file not found
errors, double-check:
- The relative path you’re using
- That your keymap.keymap
and behaviors.dtsi
are inside config/
(since only that directory is copied into the build environment).--- 👉 So yes — if you drop
behaviors.dtsi
into the same folder as your keymap.keymap
, a simple #include "behaviors.dtsi"
is all you need.Would you like me to show a minimal example of a
behaviors.dtsi
(say, a combo or tap-dance) and how it’s pulled into keymap.keymap
? That way you can be confident it’s wired up correctly.zmk already has an include called the behaviors.dtsi, so if i will be making a custom one, will i need to use a different file name?
Ah, you’ve caught an important detail 👍
You’re right — ZMK itself already has a
Best Practice - Yes, give your custom file a different name.
- Common patterns people use: -
- To avoid clobbering or confusing yourself later, it’s cleaner to give your custom one a unique name.
--- 👉 TL;DR: Don’t override ZMK’s
Would you like me to show you a small side‑by‑side example of a
You’re right — ZMK itself already has a
behaviors.dtsi
in its core, where the built‑in behaviors (like &kp
, &mo
, etc.) are defined. If you create another file with the same name, it may cause confusion or path ambiguity during the build.Best Practice - Yes, give your custom file a different name.
- Common patterns people use: -
custom_behaviors.dtsi
- my_behaviors.dtsi
- Split by type if you have a lot:
- combos.dtsi
- macros.dtsi
- tap_dance.dtsi
Then in your keymap.keymap
(or another .dtsi
) you include it like:
Why not reuse the name?
- If you call it behaviors.dtsi
, the build system might include ZMK’s version instead of yours, depending on search order.- To avoid clobbering or confusing yourself later, it’s cleaner to give your custom one a unique name.
--- 👉 TL;DR: Don’t override ZMK’s
behaviors.dtsi
. Put your own definitions in e.g. custom_behaviors.dtsi
, and include that explicitly in your keymap.keymap
.Would you like me to show you a small side‑by‑side example of a
custom_behaviors.dtsi
+ how it plugs into keymap.keymap
? That way you can be 100% sure how the separation looks.why does zmk use underscores a lot. I prefer minus signs cause they are easier to type. Is this not allowed?
Great observation 👀
This comes down to device tree naming rules (since ZMK is built on Zephyr, which uses devicetree files).
Why underscores (
- Hyphens are reserved for node names (like
- So when we write things like
- only
- must start with a letter
Example ✅ Allowed:
(Node names and comments can have
⚠️ Not Allowed in labels/behaviors: File Names For file names, technically you can use
- It avoids confusion if you later rename/move things.
- It’s the convention in ZMK repos, so examples are consistent.
--- 👉 Answer:
- Inside
- For file names → you could use
Would you like me to show you exactly where you can and can’t use hyphens inside a ZMK config (e.g. node name vs label vs filename)? That usually clears up the confusion.
This comes down to device tree naming rules (since ZMK is built on Zephyr, which uses devicetree files).
Why underscores (
_
)?
- In devicetree node labels and property names, -
(minus/hyphen) is not allowed in identifiers.- Hyphens are reserved for node names (like
/soc/gpio@40002000
) or DTS paths, not for labels or references.- So when we write things like
custom_behaviors.dtsi
or tap_dance
, we’re actually defining labels or macros, which must follow C identifier rules:- only
a-z
, A-Z
, 0-9
, and _
- must start with a letter
Example ✅ Allowed:
(Node names and comments can have
-
)⚠️ Not Allowed in labels/behaviors: File Names For file names, technically you can use
-
(e.g. my-behaviors.dtsi
). But most people stick with _
because:
- It visually matches the identifiers inside.- It avoids confusion if you later rename/move things.
- It’s the convention in ZMK repos, so examples are consistent.
--- 👉 Answer:
- Inside
.dtsi
and keymap files → you must use underscores _
for labels/behaviors.- For file names → you could use
-
, but _
is more conventional and consistent with ZMK/Zephyr.Would you like me to show you exactly where you can and can’t use hyphens inside a ZMK config (e.g. node name vs label vs filename)? That usually clears up the confusion.
i plug in the left half. I drag the new uf2 file. However, the keyboard disconnects and the changes arent made
i plug in the left half. I drag the new uf2 file. However, the keyboard disconnects and the changes arent made
The operation can’t be completed because you don’t have permission to access some of the items.
Got it — thanks for the details. This sounds like a flashing issue with the nice!nano bootloader rather than ZMK itself. Here’s what could be happening:
Why it disconnects
- The NICENANO drive always disappears immediately after you drop a
- On any macOS version, Finder can sometimes fail. Workarounds: - Use Terminal instead of Finder: - Or try a different USB cable/port. Things to check 1. Make sure you put the nice!nano in bootloader mode (double-tap reset → should show
.uf2
file — that’s normal! The bootloader reboots into firmware mode, so it looks like it “disconnects.”
- If your changes don’t appear, the UF2 either wasn’t written successfully or you flashed the wrong file (left vs right).
“Permission” error
This is common on macOS when copying files to UF2 drives:
- There is a known Finder bug on macOS 13.0 that causes permission errors. Upgrading to macOS 13.1+ fixes it.- On any macOS version, Finder can sometimes fail. Workarounds: - Use Terminal instead of Finder: - Or try a different USB cable/port. Things to check 1. Make sure you put the nice!nano in bootloader mode (double-tap reset → should show
NICENANO
drive).
2. Verify the correct half’s firmware (left
for the left side, right
for the right side).
3. Check your macOS version → update if still on 13.0.
4. If the error persists, try flashing from a Linux/Windows machine to rule out macOS Finder issues.
👉 Full guide: Entering the Bootloader
---
Would you like me to walk you through how to confirm the left-half firmware actually flashed (checking via keyboard response / ZMK Studio)? That’s the easiest way to tell if the file truly took.Entering the Bootloader | Typeractive.xyz Documentation
Learn how to enter the bootloader on your nice!nano.
board still not flashing properly
The operation can’t be completed because you don’t have permission to access some of the items.
Thanks for confirming — since you’re still getting that permission error, this really points to the macOS + Finder issue. Here’s how to work around it:
1. Use Terminal instead of Finder
Instead of dragging the UF2 in Finder, copy via Terminal:
1. Plug in your left half and double-tap reset so it shows up as NICENANO.
2. Open Terminal.
3. Run something like: (adjust the path to match where your UF2 file is saved).
4. The NICENANO drive should disappear right after — that means it flashed successfully. 2. Verify macOS version - If you’re on macOS 13.0, update to 13.1 or newer — Finder has a known bug in 13.0 that breaks UF2 flashing with “permission denied.”
- Later versions (13.1+) don’t have this bug. 3. Other things worth checking - Try a different USB cable/USB port (some cables are power-only).
- Make sure you’re flashing the left firmware UF2 to the left half (ZMK repo provides both halves separately).
--- 👉 In short:
- Stick to Terminal
- If you’re still on macOS 13.0, update macOS first.
Do you want me to give you an exact Terminal command template where you just paste your UF2 filename? That way you can copy-paste without worrying about syntax.
2. Open Terminal.
3. Run something like: (adjust the path to match where your UF2 file is saved).
4. The NICENANO drive should disappear right after — that means it flashed successfully. 2. Verify macOS version - If you’re on macOS 13.0, update to 13.1 or newer — Finder has a known bug in 13.0 that breaks UF2 flashing with “permission denied.”
- Later versions (13.1+) don’t have this bug. 3. Other things worth checking - Try a different USB cable/USB port (some cables are power-only).
- Make sure you’re flashing the left firmware UF2 to the left half (ZMK repo provides both halves separately).
--- 👉 In short:
- Stick to Terminal
cp
— this bypasses Finder’s permissions bug.- If you’re still on macOS 13.0, update macOS first.
Do you want me to give you an exact Terminal command template where you just paste your UF2 filename? That way you can copy-paste without worrying about syntax.