demiguru ·
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 apps that actively work to get around these controls. The second one is important. Every app here runs as my user, and these controls are files and processes my user controls. An app that edits my config, runs grim itself, or stops the firewall can turn them off. Everything here assumes an app uses the access the desktop gives it and doesn't go after the controls themselves. Stopping that would take sandboxing, which this setup doesn't do. 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. The portal's screenshot call asks first too. I tested it and got a dialog, and the answer gets saved. All my unsandboxed apps share that one answer, though, so a single "allow" would count for every one of them.
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.
I used to say here that the rules are only read when Hyprland starts, so a running app can't edit the config and reload its way in. That's only half true. The rules really aren't reloaded. But the switch that turns the whole permission system on is: set it to false and run hyprctl reload, or skip the file and send one hyprctl eval command, and enforcement is off for the rest of the session. I tested it in a nested Hyprland, and a screenshot tool that wasn't on my list went from a black frame to a normal screenshot. Any app running as me can talk to the socket that does this. I can't block that from the config, so the status bar now tells me when it happens (more on that below).
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.
It can also be turned off while I'm logged in. One hyprctl eval command sets it to false, and in my nested test the Signal window went from black to readable in a screenshot. So the status bar now checks every Signal window and says SIGNAL EXPOSED if one has lost the setting.
Who can see and type my keystrokes
Wayland has an input-method protocol, meant for things like typing Chinese or Japanese. Whichever app claims it first gets the keys typed into text fields. I don't use one, so nothing had claimed it, and in my test a small program with no special access claimed it and got everything I typed into a terminal. That's a keylogger, and it's exactly what my threat model is about.
Now a tiny placeholder program claims the role when I log in and does nothing with it. Any other app that asks gets turned down, and typing works the same as before. It's about 40 lines of C, and it's in the repo. If it isn't running, the status bar says IME OPEN.
Apps can also go the other way and type into other windows by creating a virtual keyboard. Hyprland lets you allow or deny keyboards by device name, and a virtual keyboard is named after the program that made it. So now only wtype, which my dictation uses, is allowed, and other virtual keyboards are denied. The name comes from the program's file name, so a renamed copy of wtype still gets through. It stops the obvious case, not a disguised one.
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, PERMS OFF means Hyprland's permission enforcement got switched off, SIGNAL EXPOSED means a Signal window can be captured again, IME OPEN means the input-method placeholder isn't running, 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.
I used to think the compositor settings were fixed once Hyprland starts, so I only checked those by hand. The permission switch above proved me wrong, so it's in the bar now. The rest I still check by hand. It also only checks that OpenSnitch is running, not that the rules are what I expect, and it polls every 30 seconds, so something could be off for up to half a minute before I hear about it.
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.
- Any app can read the clipboard. There's a clipboard-manager protocol every app can use, and Hyprland has no permission for it.
- Any app can listen to my D-Bus session bus, which includes the text of every notification, message previews too. My keyrings are locked, so that doesn't give away passwords, but stopping the listening itself would take sandboxing.
- Any app can move my mouse and click. There's a virtual pointer protocol every app can use, and Hyprland has no setting for it.
- Auto-lock isn't a guarantee. Fake mouse movement counts as activity, so any app can keep my session from locking when I walk away. I tested it in a nested Hyprland and the idle timer never ran out. Closing the lid still works, and so does locking by hand, so that's what I rely on.
- Any app can see my window titles, through the window-list protocols or Hyprland's socket. Titles often include document names, email subjects and URLs.
- Hyprland's socket is open to anything running as me. Besides switching enforcement off, it can run Lua inside the compositor, change window rules and read the cursor position.
- 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 or claim my keystrokes, 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.