A simple, transparent, open-source key logger, written in Python, for tracking your own key usage, originally intended as a tool for keyboard layout optimization.
I started looking into mechanical keyboards and the variation in layouts—QWERTY, AZERTY, Colemak, Dvorak, etc.—is just the beginning. You can very rapidly descend into fascination/madness with layers, hotkeys, tap-mods, and more, especially as you get down from full-size (100+ keys) boards to the smaller ones (like 36-key boards, and sometimes even less). All of this is based on making your typing optimal, comfortable, fast, and maybe a few other personally important adjectives.
One key input to these choices is knowing what keys and combos you really use most often. A key logger is a convenient way to self-analyze and see what your key usage looks like in practice. (As opposed to simply analyzing language averages, or short samples of work.) Across people, it could vary greatly depending on what language you work in (English, Swedish, Portuguese), and whether you're a programmer, author, etc. So, when I tried to find a key logger for this purpose, most of what I found was tagged with headlines like "get credit card info..." or "catch your cheating girlfriend". Moreover, and perhaps more importantly, they were either closed-source, executable files, or too complicated to understand. (I'm curious about my typing, but not enough to take even a small risk of putting an actually nefarious key logger on my system!)
Make a key logger simple enough that a moderately experienced programmer can quickly read through, understand, and be convinced that nothing nefarious is going on.
Use standard and/or known libraries, and use as few as possible.
Keep all data local and simply consolidated. Send nothing, anywhere, off the computer.
Comment the code extensively to explain not only what's happening, but additionally the thinking behind each choice.
The code is still written in the spirit of transparency, but these days that clarity comes from a mix of comments and explicit structure. The script is organized around a small configuration object and a single application object so the runtime state is easier to follow than when everything lived in globals. I still want every important decision to be easy to audit, both in what it does and why it's there.
I use this on my Mac, running on Python3 (3.8.1, but I presume any Python3 version would work), and haven't tried it on any other operating system. I presume it would work, but it seems easy to believe that there are details that I don't know of. If anyone tries it and finds ways to improve/extend it, that'd be great.
There are two ways to store the output (the key log). In both cases, it's put in a single file in the same directory you're running the script.
- Put every key stroke into a text file, one entry per line
- Put every key stroke into a SQLite database
SQLite is the default option. You can swap that or turn both on if you wish. The main storage options are exposed through the command line now instead of requiring you to edit the script.
Because the output contains sensitive data, the logger now creates its log files with owner-only permissions (0600) and will tighten existing log files if they are more permissive. That applies to the text log, the main SQLite file, and the common SQLite sidecar files (-journal, -wal, and -shm) if they exist.
I chose SQLite because the output is a single file that you can delete anytime you want, and it doesn't require any separate database engine. If you're not familiar with it, it's much like putting your data in a text file, one entry per line, but it does so in a structured way that, when you use a program that knows how to read that structure, gives you the power of SQL. The nice thing is that 100% of the data, meta data, etc. is in that one file. And having the entries in a database, does provide some advantages (if you know SQL) when you want to answer questions like "Show me the keys I press in descending order, by frequency?" or "What percentage of key strokes is the space bar?" You can even do some fun stuff like "How fast do I type?" (since timestamps are maintained in the SQLite log), "Do I type more during odd or even hours of the day?", and other, life changing questions-and-answers. A basic version of this is built into the SQLite output file, in the form of predefined views.
To avoid paying the full SQLite commit cost on every single keystroke, the logger batches writes and commits them every 50 events or every 5 seconds, whichever comes first, and then performs a final flush on clean shutdown. The event threshold is chosen to be roughly consistent with a fast typist around 100 WPM.
If you want better behavior while inspecting the database at the same time the logger is writing to it, there is also a --wal option. It is off by default to keep the file model as simple as possible. If you turn it on, SQLite uses write-ahead logging, which can improve read/write concurrency, but it also means you should expect sidecar files like -wal and -shm to appear while the database is active.
By default the logger records the resulting character or combo, so Shift+a is logged as A. If you would rather log the physical key plus modifiers, use --physical-keys. In that mode, Shift+a is logged as <shift> + a, and similarly Shift+1 is logged as <shift> + 1.
By default the plaintext file log prepends a UTC ISO timestamp to each entry. If you want the old simpler plaintext format, use --no-file-timestamps.
By default left and right modifiers are still remapped together in the log (<shift_l> and <shift_r> both become <shift>, and similarly for Control, Alt, and Command). If you want to preserve those distinctions, use --modifier-sides.
Among other options, two applications I use to look at and query the SQLite data file are
The Python logging module is used to provide INFO, DEBUG, WARNING, etc. messages. Those operational messages still go to the screen, not to a file. However, the actual captured key strokes are not echoed to stdout by default anymore. Terminal scrollback often persists much longer than people expect, so printing every key press by default creates a second plaintext copy of the data. If you want to watch the key stream in the terminal while the logger runs, you can opt in with the --stdout flag described below. If you want to see the program's internal state transitions, remapping decisions, batching activity, and similar implementation details without echoing your keystrokes, use --debug. By default the logger shows only INFO (and above) messages. If you want the logged content itself to represent physical keys instead of resulting characters, use --physical-keys.
I only know how (and even if you need) to grant keyboard access on a Mac. You must give the script Accessibility permissions in System Preferences > Security & Privacy > Privacy > Accessibility. (Don't forget that you likely need to unlock this Preferences screen to make any changes.) This gives the app you run it from permission to see the keyboard events. I usually use the Terminal, but you can also run it from your code editor. Without this step, the script will just sit there silently, deaf to all keyboard events. macOS automatically suppresses logging when it switches into Secure Input Mode (passwords). So, through no effort of my own, it very nicely avoids logging any information typed into OS-labeled password text boxes. At least for me, this is even true for password fields in my browser. Nice! (I don't know if Windows or Linux has anything comparable, so if you use it there, be aware that your passwords may or may not be tracked by the logger.)
You'll need to install pynput. You can see more details on that library from PyPi or GitHub, and you can read its documentation as well. The other items are all Python-standard libraries: datetime, logging, and sqlite3. I purposefully do not put pynput here in this repo because I don't want you to have to trust that the version included hasn't been tampered with. You can use pip to install it: pip3 install pynput.
I run it from the Terminal with python3 key_logger.py. If you explicitly want the captured keys echoed to stdout while the program runs, use python3 key_logger.py --stdout.
You can see the available options at any time with python3 key_logger.py --help.
The help output shows the current defaults, and the program prints a short startup summary of the effective configuration and output paths when it begins listening.
Some common examples:
- Default SQLite logging:
python3 key_logger.py - Physical key logging instead of resulting characters:
python3 key_logger.py --physical-keys - Preserve left/right modifier distinctions:
python3 key_logger.py --modifier-sides - Inspect the logger's internal behavior without echoing captured keys:
python3 key_logger.py --debug - SQLite plus plaintext log file:
python3 key_logger.py --file - Plaintext file with timestamps:
python3 key_logger.py --file - Plaintext file without timestamps:
python3 key_logger.py --file --no-file-timestamps - Plaintext file only:
python3 key_logger.py --no-sqlite --file - SQLite with the verbose full event table:
python3 key_logger.py --full-events - SQLite with WAL enabled for concurrent inspection:
python3 key_logger.py --wal - Physical key logging plus live echo:
python3 key_logger.py --physical-keys --stdout - Physical key logging with left/right modifier distinctions:
python3 key_logger.py --physical-keys --modifier-sides - Show both internal debug output and live key echo:
python3 key_logger.py --debug --stdout - Custom output filenames:
python3 key_logger.py --sqlite-file my_keys.sqlite --log-file my_keys.txt --file
You could add execution permissions to the file (chmod +x key_logger.py) and then run it like a script (./key_logger.py), since it does have the Python shebang at the top; however, in the spirit of being benign, I don't like the idea of making the file executable, even though I know it's not an EXE, but ¯\_(ツ)_/¯.
This tool does not send your data anywhere, but the files it writes are still sensitive. They contain raw keystrokes and should remain owner-only on disk. The program now enforces that automatically for the files it creates and warns when it has to tighten existing permissions, which helps reduce local disclosure risk on shared machines or under a permissive umask.
If you want a quick trust checklist before running it, here are the main things to verify:
- No network behavior: the script imports no networking libraries and sends nothing anywhere.
- Debug output is opt-in:
--debugenables internal state logging but does not imply key echo. - Physical key logging is opt-in:
--physical-keysswitches from logging the resulting character to logging the physical key plus modifiers. - Left/right modifier distinction is opt-in:
--modifier-sideskeeps modifier sides separate instead of remapping them together. - Stdout echo is off by default: keystrokes are only printed to the terminal if you pass
--stdout. - SQLite is on by default: the main log goes to a local SQLite file unless you disable it with
--no-sqlite. - Plaintext file logging is off by default: the text log is only enabled with
--file. - Plaintext timestamps are on by default:
--no-file-timestampsreverts the plaintext file log to bare key entries. - Output files are owner-only: the logger creates or tightens log files to
0600, including SQLite sidecar files when present. - Full event capture is opt-in:
--full-eventsenables the more verbose key up/down table in SQLite. - WAL is opt-in:
--walenables SQLite write-ahead logging for concurrent inspection. - Password handling depends on the OS: on macOS, secure input mode usually suppresses logging in password fields, but that behavior is provided by the OS, not by custom filtering in this script.



