What a Wayland compositor can and can't protect you from
Most Hyprland configs you find online are about how the desktop looks. Mine was too, for a while. At some point I started asking a different question: what is this setup actually protecting me from, and where does that stop?
This post is what I came up with. It's my daily driver on Arch, and the full config with notes on every control is on Codeberg: demiguru/Hyprland-threat-model.
Who I'm defending against
I'm not worried about a nation-state. What I want to limit is commercial data collection: apps I installed on purpose that do more than I asked them to. That means apps, and the SDKs inside them, that capture the screen, look at other windows, log input or send data somewhere. They don't need exploits for any of that. They run as my user like everything else.
So what I care about is what's on my screen, what's in my messages, what I type, and which hosts my machine talks to.
What I'm not covering: physical access, and anything that already runs code as my user. The second one is important. All of these controls are config files my user can edit. If something can run code as me, it can turn every one of them off. A compositor config can limit what normal apps can see and do. It can't make an untrusted program safe to run, and I'd rather say that up front.
XWayland is off
Wayland already keeps apps from reading each other's windows or faking input. X11 doesn't. Any X client can see what you type into another one, read its contents and send it input. XWayland brings that back for every app that uses it, and they all share one X server.
I turned XWayland off. The cost is that anything without native Wayland support won't start. I checked, and nothing I use needs it. If you depend on even one X11 app, this probably isn't for you, because running XWayland for that one app brings the shared server back for everything.
Screen capture only for apps I listed
Hyprland has a permission system, and when you set it to enforce you can allow screen capture per binary. This is my whole list:
hl.permission("/usr/bin/grim", "screencopy", "allow")
hl.permission("/usr/bin/wf-recorder", "screencopy", "allow")
hl.permission("/usr/bin/hyprpicker", "screencopy", "allow")
hl.permission("/usr/lib/xdg-desktop-portal-hyprland", "screencopy", "allow")
hl.permission(".*", "screencopy", "deny")
Browsers and video calls still work because they go through the portal, where I pick what to share.
This is also the control I'm most likely to give too much credit to. The rule checks the path, not who is running it. grim is allowed, so anything running as me can run grim and get a screenshot. What it does stop is an app quietly grabbing the screen by itself, which is the thing I'm worried about. Just don't confuse it with a real security boundary.
One thing I liked: the rules are only read when Hyprland starts. Changing the file and reloading doesn't apply them. That's how it should be, otherwise a running app could give itself access by editing the config and asking for a reload. It only protects the running session, though. The config is my file, so a change made today would still take effect at the next login.
The same system limits plugins. Plugins are shared libraries loaded into the compositor, so they see everything. Only hyprpm (Hyprland's plugin manager) is allowed to load them; everything else is denied. That controls who can load code into the compositor. It says nothing about whether the plugins I allowed are any good.
Signal stays out of screen shares
A window rule blanks Signal in screen shares and recordings. This one is more about mistakes than attackers, like sharing my whole screen in a call with a chat open. It only covers Signal, and it doesn't hide message previews in notifications.
Outbound traffic: deny by default
Everything above limits what apps can see. But collecting data only matters if it gets sent somewhere, so the control that actually deals with my threat model is OpenSnitch, an application firewall. It runs with deny as the default. If a program has no rule and tries to connect out, the connection is blocked and I get a prompt.
It's not free. There are a lot of prompts at first, updates change paths and behavior, and when you're tired you just click allow. The bigger limit is that rules are per program. Once the browser is allowed out, anything that can drive the browser is allowed out too. The firewall can't tell my browser doing what I asked from my browser doing what some extension asked.
I'm not publishing my rules. They're basically a list of everything I run and where it connects, which is exactly the kind of information I'm trying to keep to myself.
Knowing when something stopped working
I didn't plan this part. It came from experience: two of my protections had already stopped working at some point and nothing told me. Once my VPN's lockdown mode was off, and once OpenSnitch wasn't there to stop a sync service from being reachable from the internet. Both times I kept going as if everything was fine. A firewall that didn't start is worse than no firewall, because you still think you have one.
So now a small module in the status bar checks every 30 seconds and shows one word: SEC OK, or the first problem it finds. FW DOWN means OpenSnitch isn't running, VPN DOWN means the tunnel is down, and NO LOCKDOWN means the VPN's kill switch is off. When the state changes I also get a notification, so I find out even if I'm not looking at the bar.
It only checks things that can break while I'm logged in. The compositor settings are fixed once Hyprland starts, so for those I have manual checks instead. It also only checks that OpenSnitch is running, not that the rules are what I expect.
Moving the config to Lua
Hyprland is replacing its old config format (hyprlang) with Lua, and I moved everything over in one go. A few things I learned:
My generated files became code. A script picks an accent color from my wallpaper and writes a small file that the config loads. With hyprlang that was just a few variables. With Lua it's a program running inside the compositor. I load it with loadfile from an explicit path and an empty environment, not with require, because require searches a path that something else could put a file into, and it caches the result. And I treat the script that writes it as part of the compositor now.
One mistake used to break everything. hyprlang skips a bad line and keeps going. Lua compiles the whole file at once, so one typo anywhere and the whole config fails to load. I split it into modules (permissions, keybindings, window rules and so on), and hyprland.lua now just loads them one at a time with pcall. If one breaks, I get a notification and the rest still loads.
The move found an old bug. I had a keybind for the laptop lid that ran systemctl suspend, and logind was already set to suspend on lid close. So every time I closed the lid it got told to suspend twice. It was sitting in my old config and I never noticed. Going through every line during the move is what caught it.
I test changes before they go live. A small script starts the new config as a nested Hyprland window inside my session, checks it, and closes it. It can't take over the real display. It's had its own bugs. For a while, every test run switched my real VPN server, because the test instance ran my autostart too. Another time it ended up testing my live config instead of the new one. Both are fixed now, but I mention them because the test tool needed testing too.
What it doesn't cover
- Anything running as my user can undo all of it.
- The capture rules can be bypassed by running an allowed program.
- There's no app sandboxing. Wayland separates apps on the display, but they can still read my files, use my D-Bus session and see each other's processes. That would need Flatpak, bubblewrap or separate users, and I don't have any of that.
- Clipboard history is saved to disk. Copies from my terminal are marked sensitive so the history skips them, but copies from other apps are still kept.
- Notification previews show message text no matter what the capture rules say.
- Whatever the firewall allows, it trusts.
So is it worth it?
For what I'm worried about, I think so. Normal apps can't quietly capture my screen, there's no shared X server, only the plugin manager can load code into the compositor, and anything phoning home has to get past the firewall. It doesn't make bad software safe, and it does nothing against something already running as me.
If you want to do something similar, I'd start the same way I did: write down who you're defending against, then for each setting write down what it costs and where it stops working, and make sure you find out when one of them breaks.
Everything is on Codeberg, with the reasoning, costs, limits and how to check each control: demiguru/Hyprland-threat-model.