While testing something I ssh’d into muOS (TrimUI Brick) and ran htop. I noticed that the hotkey.sh script was saturating one core of the CPU almost entirely.
With the script running and nothing else happening on the device (except the frontend), average CPU usage is 30%+. Killing the script brings it back down to 5-6%.
I did some debugging and found the script is doing some kind of busy-wait poll. Running strace on it floods the terminal with poll() and read() calls whereas its underlying tool muhotkeys doesn’t exhibit this behaviour (it’s properly idle cuz no keys are being pressed). Since I’m not familiar with muOS internals to this degree I let Claude and Codex ssh into my Brick and debug it independently. They both arrived at the conclusion that a 5-sec sleep inside idle.shkeeps getting interrupted by SIGCHLD signals, causing that 5 sec wait to effectively become a busy wait loop.
I think this is a huge problem for performance as it’s always using up at least 20-25% of cpu power when nothing is happening, causing unnecessary heat and battery drain. The script should surely be optimised! I didn’t find any “Issues” tab enabled on any of the muOS GitHub repositories, hence posting here.
If you’re interested here are more detailed post mortems:
Codex debugging
-
hotkey.sh itself is a launcher/dispatcher, but it also sources idle.sh at hotkey.sh (line 5).
-
idle.sh starts a background infinite loop (idle.sh (line 31) to (line 60)) that:
-
scans every /proc/[0-9]*/comm ( (lines 46-48))
-
calls GET_VAR and SET_VAR each pass ( (line 34), (line 58))
-
-
GET_VAR/SET_VAR are implemented as background subshells ((…) &) in func.sh (lines 65-87).
-
That causes constant SIGCHLD, which interrupts sleep 5 in idle.sh:
- strace shows clock_nanosleep(…5s…) repeatedly interrupted almost immediately (ERESTART_RESTARTBLOCK), so loop runs nearly continuously.
-
This is why hotkey.sh burns CPU and floods read/poll in strace, while muhotkey stays mostly idle.
Claude Code debugging (probably clearer)
Confirmed. sleep 5 returns in 0 seconds. SIGCHLD from ( ) & in SET_VAR/GET_CONF_PATH immediately interrupts busybox ash’s sleep builtin.
This is the root cause of the entire CPU issue:
idle.sh loop:
1. GET_VAR → spawns ( ) & subshells
2. /proc scan: ~90ms at ~90% CPU
3. SET_VAR → spawns ( ) & → completes in ~1ms → SIGCHLD fired
4. sleep 5 → interrupted immediately by SIGCHLD → returns in 0ms
5. goto 1
Instead of running every 5 seconds, the loop runs every ~90ms — roughly 55x faster than intended. That maps almost exactly to the observed CPU usage.
The bug is actually in func.sh — GET_CONF_PATH using ( ... ) & (background subshell) is inherently racy and poisons every script that uses GET_VAR/SET_VAR and then sleeps. Worth reporting this to muOS upstream as it likely affects other scripts too.
The minimal fix for idle.sh is to ignore SIGCHLD around the sleep:
SET_VAR "system" "idle_inhibit" "$INHIBIT"
trap '' CHLD
sleep 5
trap - CHLD
Or avoid SIGCHLD altogether by waiting for the SET_VAR subshell before sleeping:
SET_VAR "system" "idle_inhibit" "$INHIBIT"
wait
sleep 5
Though wait here would also catch any other stray background jobs, so the trap '' CHLD approach is safer.