The Keyboard That Wasn't Broken

The Keyboard That Wasn't Broken

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.

The keyboard was never broken — the input-method socket was

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.

The failure chain
The failure chain

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() returns ENOENT → 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.

Evidence
Evidence


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".

Timeline
Timeline


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 culprit
The culprit

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.

Three layers of defence
Three layers of defence

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

  1. Prove the boundary before theorising. xev proved the X server was innocent in 30 seconds and turned a hardware hunt into a software hunt.
  2. Read the application's stderr. The exact cause was in ~/.xsession-errors for four hours.
  3. Build a probe that prints a diff. "It doesn't work" → entry_text='' → a hypothesis you can test in ten seconds.
  4. 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.
  5. Fix the class, not just the symptom. Restarting ibus fixed the outage; moving the socket to /run/user/1000 fixed the class of outage.
  6. "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.
  7. 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.
  8. Birth times are a forensic superpower. stat -c %w on a directory's contents turns "something deleted my file" into a precise minute of the wipe.

Related upstream

ibus/ibus#2930 — open (Aug 2026)
#2793 — socket path → $XDG_RUNTIME_DIR (reverted)
#2204 · #2188 · #2095

#Linux#LinuxDesktop#LinuxMint#Ubuntu#SysAdmin#DevOps#Troubleshooting#Debugging#SystemD#X11#D-Bus#InputMethod#ShellScripting#Automation#OpenSource#DevCommunity#SRE#LinuxTips

Reproduction environment: Linux Mint 22.3 (Cinnamon 4.x, X11) · ibus 1.5.29 · GTK 3.24 · Qt 5.15 · kernel 7.0.0-38 · systemd 256 · HP 290 G4 (i5-10500, 31 GB RAM, Intel UHD 630).