Deep-dive debugging · Linux desktop input
The Keyboard That Wasn't Broken
How a deleted temp file in ~/.cache killed every text field on a Linux desktop — and how to find it in minutes instead of hours.

TL;DR
ibus keeps its client socket in ~/.cache/ibus/dbus-XXXXXX. A cache-cleaner
(Stacer's “Application Caches”) deleted the whole contents of ~/.cache, so every
new input-method connection failed with ENOENT — and GTK/Qt apps silently
swallowed every keystroke while the terminal kept typing. The fix: run ibus with
XDG_CACHE_HOME=/run/user/1000/ibus-cache so the socket lives in tmpfs, plus a
60-second watchdog and GTK_IM_MODULE=xim as defence in depth. Upstream bug:
ibus/ibus#2930 (open).
The complaint
"Some times the keyboard doesn't work on CLI tools but some times in graphical tools like browser text edit and all GUI tools. Now the keyboard is not working in graphical UI tools."
That sentence contains everything you need and almost nothing you can act on. "Sometimes", "some times", "all GUI tools". It reads like a hardware fault, a focus bug, a window-manager quirk, or a dying USB receiver.
It was none of those. It was one file, and it was not the file anyone would guess.
Step 0 — refuse the obvious hypotheses
The first three things a Linux engineer checks when "the keyboard doesn't work" are the right three to check first, precisely because they are cheap:
| Hypothesis | How to kill it | Verdict |
|---|---|---|
| Dead keyboard / flaky USB receiver | dmesg \| grep -i usb, /proc/bus/input/devices |
✅ healthy — Logitech Unifying receiver, no disconnects, no resets |
| Broken XKB layout / wrong keymap | setxkbmap -query |
✅ us,us, pc105, nothing exotic |
| X server not receiving keys | xev on a scratch window, then type |
✅ every keystroke arrived, 14/14, synthetic NO |
At this point the honest conclusion is: the input reaches the X server and the X server delivers it. So the loss is happening inside the applications. That single sentence turns a hardware hunt into a software hunt, and it is worth stating out loud before touching anything else.
Step 1 — find the boundary
The next question is not "what is broken" but "where does it break". The fastest way to get that answer is to stop guessing and start measuring a text field.
I wrote a 25-line harness: launch a GTK entry dialog, focus it, inject a known string with
xdotool, and read back what the widget actually received:
zenity --entry --title=KBTEST & # a plain GTK text field
xdotool windowactivate --sync $(xdotool search --pid $! | head -1)
xdotool type --delay 80 "PASS-test" # inject a known string
# ... then read what the entry box contains
Result, printed as a single line:
default: focus='KBTEST-default' entry_text='' IBUS-WARNING: Unable to connect to ibus:
Could not connect: No such file or directory
default: focus='KBTEST-default' entry_text='' IBUS-WARNING: Events queue growing too big,
will start to drop.
The application received zero characters and told us exactly why. That stderr line is the whole
story, and it had been sitting in ~/.xsession-errors since 16:05 that same day, from an unrelated
app (mintUpdate.py), waiting to be read:
(mintUpdate.py:9322): IBUS-WARNING **: 16:05:55.444: Unable to connect to ibus:
Could not connect: No such file or directory
Lesson 1. A silent symptom deserves a loud probe. One script that injects a known string and prints the result turns "the keyboard is broken" into a two-line diff.
Step 2 — follow the error to its source
ibus is the input-method daemon. Clients find it through an address file:
~/.config/ibus/bus/62f97850e5fb488e81af14bba903fc65-unix-0
IBUS_ADDRESS=unix:path=/home/anil/.cache/ibus/dbus-c9fMEEgj,guid=fa06fa98...
So the daemon told every client: "connect to /home/anil/.cache/ibus/dbus-c9fMEEgj".
And that path did not exist:
$ ls -la ~/.cache/ibus/
ls: cannot access '/home/anil/.cache/ibus/': No such file or directory
Now the strange part. The daemon was alive. It still owned org.freedesktop.IBus on the session
bus. It still had the socket open in its own file descriptors (ss -xlp showed the LISTEN socket).
It simply had no name on the filesystem any more — a UNIX socket whose inode is unlinked is
unreachable by path, but perfectly reachable by anyone who already holds a connection to it.

That single fact explains the whole "sometimes it works" behaviour:
- Apps launched before the deletion still hold an established connection → they keep typing.
- Apps launched after the deletion open a new connection →
connect()returnsENOENT→ the input-method context is alive but blind → it queues key events and never commits them → the text field stays empty forever.
The symptom was therefore a function of app launch time, not of what you typed. That is the
signature of a ghost bug, and it is why "the keyboard is broken in the browser but not in the
terminal" felt so arbitrary: gnome-terminal uses VTE, which builds its own
gtk_im_context_simple and never talks to ibus at all.

Step 3 — why the socket lives in your cache directory (and why that's the bug)
This is not a misconfiguration on my machine; it is ibus's compiled-in default:
$ strings /usr/bin/ibus-daemon | grep tmpdir
unix:tmpdir=$XDG_CACHE_HOME/ibus
The cache directory is, by definition, the directory software is allowed to wipe. Upstream knows this is a wart:
| Upstream issue | Title | State |
|---|---|---|
| ibus/ibus#2930 | "ibus-daemon doesn't detect/recover when its own socket directory is deleted while running (silent, inconsistent-looking keyboard-input failure)" | open (Aug 2026) |
| ibus/ibus#2793 | "Set default socket path to $XDG_RUNTIME_DIR?" |
closed — the change was reverted in 5742e1b |
| ibus/ibus#2204 | "Move ~/.config/ibus/bus/ to XDG_RUNTIME_DIR?" |
closed (completed) |
| ibus/ibus#2188 | Release notes for 1.5.22 are confusing | closed — the socket-dir change caused confusion |
The reason #2793 was reverted is deliciously ironic: the ibus maintainer wrote
"I found I have 560 stale dbus socket in my $HOME/.cache/ibus/ folder that was accumulated over
two years of use." — i.e. moving the socket to $XDG_RUNTIME_DIR was reverted because the
cache-dir socket had been accumulating garbage for two years, which is exactly the same
cache-dir-rot problem that bit me.
And #2930 is my bug, reported by someone else in August 2026, on a different desktop, with the same socket path, the same error string, the same "silent and inconsistent across applications", and — notably — the same conclusion: "We were not able to determine what deleted it."
So: known bug, still open upstream. A maintainer replied "Interesting RFE … it's nice to have the solution", which in upstream language means "real, minor, not yet fixed".

Step 4 — the culprit: my own "clean temporary files" button
The socket had to be deleted by something. The honest answer was hiding in plain sight: I had
run a cache cleaner that morning — Stacer's System Cleaner → Application Caches, which is bound
to QStandardPaths::CacheLocation, i.e. ~/.cache, and which deletes the entire contents of
that directory. ~/.cache/ibus/dbus-XXXXXX went with it.
One command proves it, using birth times instead of modification times:
$ stat -c "%w %n" ~/.cache/ | head
2025-09-25 11:32:32 ~/.cache/ <- the directory itself survived
$ for d in ~/.cache/*/; do stat -c "%w %n" "$d"; done | sort
2026-10-23 16:01:0 ~/.cache/codex-runtimes/ <- everything inside was recreated
2026-10-23 16:01:3 ~/.cache/fontconfig/ <- after a single wipe at ~16:01
2026-10-23 16:01:4 ~/.cache/mesa_shader_cache/
2026-10-23 16:02:3 ~/.cache/mozilla/
2026-10-23 16:04:0 ~/.cache/flatpak/
2026-10-23 16:05:7 ~/.cache/mintinstall/
Nothing inside ~/.cache is older than 16:01, while the directory dates from September. The
contents were emptied and rebuilt in a single minute — and the ibus socket was born at 15:58 and
died at 16:01. Four minutes later mintUpdate.py printed the first error.

The most likely cause of an "impossible" event is usually the one you already did and forgot to mention, because to you it seemed unrelated to the symptom. A cache clean at 16:01 and a symptom at 16:05 is a four-minute gap; without the timeline it looks like a ghost.
Step 5 — the repair, in four layers
A repair should do two things: make it work now and make it impossible to happen again.
Layer 0 — move the fragile resource out of the fragile place (the actual cure)
ibus compiles in the socket location:
$ strings /usr/bin/ibus-daemon | grep tmpdir
unix:tmpdir=$XDG_CACHE_HOME/ibus
The cache directory is, by definition, the one directory software is allowed to wipe. So: run ibus with its cache pointed at the runtime directory instead.
# ~/.config/systemd/user/ibus-daemon.service
[Service]
Environment=XDG_CACHE_HOME=/run/user/1000/ibus-cache
ExecStartPre=-/usr/bin/pkill -x ibus-daemon
ExecStartPre=/bin/sleep 1
ExecStartPre=-/bin/sh -c 'rm -f /run/user/1000/ibus-cache/ibus/dbus-* 2>/dev/null'
ExecStart=/usr/bin/ibus-daemon --panel disable --xim
Restart=on-failure
The socket now lives at /run/user/1000/ibus-cache/ibus/dbus-XXXXXX — tmpfs, removed only at
logout. Verified by doing exactly what Stacer does:
$ rm -rf ~/.cache/*
$ ibus engine
xkb:us::eng # still reachable
$ # GTK text field: entry_text='PASS-clean_after_wipe'
Wiping the cache can no longer affect input at all.
Layer 1 — heal it (watchdog)
#!/bin/bash
# ~/.local/bin/ibus-keepalive.sh
ADDR_FILE=$(ls -1t "$HOME"/.config/ibus/bus/*-unix-0 | head -1)
SOCK=$(grep -h '^IBUS_ADDRESS=' "$ADDR_FILE" | sed -n 's/.*unix:path=\([^,]*\).*/\1/p')
if [ -n "$SOCK" ] && [ -S "$SOCK" ] && timeout 5 ibus engine >/dev/null 2>&1; then
exit 0 # healthy
fi
systemctl --user restart ibus-daemon.service
Run from a systemd user timer every 60 seconds. One trap worth documenting: the unit needs
KillMode=process, because systemd's default cgroup cleanup reaps the freshly daemonized
ibus-daemon the instant the oneshot finishes — the watchdog then appears to "do nothing" while
silently restarting a daemon that dies one second later. Verified by deleting the socket and
watching it come back.
Layer 2 — don't depend on the fragile path at all
GTK's ibus input module needs that socket file. GTK's xim module does not: it talks to
ibus-x11 over the X protocol, and if ibus is gone entirely it degrades to GTK's built-in
simple context.
# /etc/X11/Xsession.d/98-gtk-xim
if [ -z "$GTK_IM_MODULE" ]; then
GTK_IM_MODULE=xim
export GTK_IM_MODULE
fi
Measured, with the socket deliberately deleted and the daemon still running:
default IM (ibus module) -> entry_text='' ✗ dead
GTK_IM_MODULE=xim -> entry_text='PASS-sim_xim' ✓ immune
ibus fully stopped -> entry_text='PASS-xim_noibus' ✓ still types
Qt apps on this box have no xim plugin (only ibus and compose), and compose was
measured dropping characters (COMPOSE-TEST-ABC → COSE-QT-OK), so Qt stays on ibus — which is
now safe anyway, because Layer 0 moved the socket out of harm's way.
Layer 3 — prove it, don't assume it
The same harness that found the bug is now the regression test. Every change is validated by breaking the system on purpose and checking that the fix survives.

The operational part
Do not run Stacer's "Application Caches". It empties the whole of ~/.cache — every app's
cache, not just junk — and it is a live landmine for any daemon that keeps a socket there
(input methods, D-Bus services, browsers, Electron apps). Deselect that one category and keep
using the cleaner for everything else.
The 30-second health check
ibus engine # must print: xkb:us::eng
ls -la /run/user/1000/ibus-cache/ibus/ # must show a dbus-XXXXXX socket
If either fails:
systemctl --user restart ibus-daemon.service
What upstream knows
This is a known, still-open upstream bug — not a private mystery:
| Issue | Title | State |
|---|---|---|
| ibus/ibus#2930 | "ibus-daemon doesn't detect/recover when its own socket directory is deleted while running (silent, inconsistent-looking keyboard-input failure)" | open (Aug 2026) — a maintainer replied "Interesting RFE … nice to have the solution" |
| ibus/ibus#2793 | "Set default socket path to $XDG_RUNTIME_DIR?" |
closed — the change was reverted in 5742e1b |
| ibus/ibus#2204 | "Move ~/.config/ibus/bus/ to XDG_RUNTIME_DIR?" |
closed (completed) |
| ibus/ibus#2188 | Release notes for 1.5.22 are confusing | closed — the socket-dir change caused confusion |
And the history is deliciously ironic: the attempt to move the socket to $XDG_RUNTIME_DIR was
reverted because the ibus maintainer had found "560 stale dbus sockets" accumulated in
$HOME/.cache/ibus/ over two years. The fix for the cache-dir rot was undone because of the
cache-dir rot.
The one thing upstream does not know is how to make the daemon survive this without user
supervision. My Layer 0 (socket in the runtime dir) and Layer 1 (watchdog) are exactly that
proposal, and #2930's author even wrote a watchdog of their own. If you hit this, the issue is
worth a comment with your numbers — especially the chattr +i failure (the filesystem does not
support immutable flags) and the GTK-vs-Qt asymmetry.
Takeaways
- Prove the boundary before theorising.
xevproved the X server was innocent in 30 seconds and turned a hardware hunt into a software hunt. - Read the application's stderr. The exact cause was in
~/.xsession-errorsfor four hours. - Build a probe that prints a diff. "It doesn't work" →
entry_text=''→ a hypothesis you can test in ten seconds. - Reproduce the failure deliberately. Deleting the socket turned a ghost bug into a deterministic, 30-second test case — and that is what made the fix provable.
- Fix the class, not just the symptom. Restarting ibus fixed the outage; moving the socket to
/run/user/1000fixed the class of outage. - "Sometimes it works" is a clue, not noise. Inconsistent behaviour across apps almost always means the difference is when the app started, not what it does.
- Ask what the user did before hunting an unknown actor. The answer was in the first sentence of their next message; the filesystem confirmed it in one command.
- Birth times are a forensic superpower.
stat -c %won a directory's contents turns "something deleted my file" into a precise minute of the wipe.