<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Code is Data, Data is Code - Medium]]></title>
        <description><![CDATA[Functional programming, Lisp languages, symbolic computing, and better ways to think about data. - Medium]]></description>
        <link>https://medium.com/code-is-data-data-is-code?source=rss----c7439eb7d33b---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Code is Data, Data is Code - Medium</title>
            <link>https://medium.com/code-is-data-data-is-code?source=rss----c7439eb7d33b---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 05:00:37 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/code-is-data-data-is-code" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[I Built a Python Tool That Watches What My Computer Does Behind My Back]]></title>
            <link>https://medium.com/code-is-data-data-is-code/i-built-a-python-tool-that-watches-what-my-computer-does-behind-my-back-7419978911c9?source=rss----c7439eb7d33b---4</link>
            <guid isPermaLink="false">https://medium.com/p/7419978911c9</guid>
            <category><![CDATA[cybersecurity]]></category>
            <category><![CDATA[privacy]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[linux]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Aeon Flex, Elriel Assoc. 2133 [NEON MAXIMA]]]></dc:creator>
            <pubDate>Sat, 24 Oct 2026 22:04:57 GMT</pubDate>
            <atom:updated>2026-10-24T22:04:56.114Z</atom:updated>
            <content:encoded><![CDATA[<h4>An experiment in process monitoring, network connections, and the enormous amount of perfectly ordinary nonsense happening on a Linux machine.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PSPxFAzdxL48CBHDMwOwjw@2x.jpeg" /><figcaption>Photo by Juanjo Jaramillo on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Your computer is doing things right now that you didn’t ask it to do.</p><p>Not necessarily malicious things. That’s the annoying part.</p><p>Somewhere in the background, a service is checking whether another service is alive. Your desktop environment is talking to a local socket. A browser process you thought you closed is still hanging around. Something is resolving a hostname, something else is maintaining a connection, and systemd has probably started three processes whose names look like rejected pharmaceutical products.</p><p>Most of this is normal.</p><p>But how would you actually know?</p><p>Linux gives us ps, top, ss, lsof, journal logs, /proc, and enough diagnostic utilities to make a small civilization out of troubleshooting. They’re excellent tools. They’re also mostly snapshots, unless you arrange to collect their output continuously.</p><p>I wanted something slightly different: a Python program that would quietly record what was happening on my machine, timestamp changes, associate network connections with processes when possible, and leave behind a readable history.</p><p>No cloud dashboard. No subscription. No enormous security platform demanding half the machine’s RAM before it will tell me that Firefox connected to the internet.</p><p>Just Python, psutil, SQLite, and a little suspicion.</p><p>The interesting part turned out to be less about catching anything sinister and more about understanding what suspicious activity actually looks like when it’s surrounded by thousands of completely ordinary events.</p><h3>The problem with looking only when something seems wrong</h3><p>Suppose your laptop suddenly gets hot.</p><p>You open a terminal.</p><pre>top</pre><p>Everything looks normal.</p><p>You check network connections:</p><pre>ss -tunap</pre><p>Nothing particularly interesting.</p><p>You look at running processes:</p><pre>ps aux  — sort=-%cpu | head</pre><p>Firefox is eating memory. Shocking development.</p><p>The problem is that you’ve started investigating after noticing something unusual.</p><p>If a process ran for twelve seconds, opened a connection, transferred data, and exited, it might already be gone.</p><p>A snapshot can’t tell you what happened before you took it.</p><p>This is why security monitoring systems maintain event histories instead of relying entirely on live dashboards.</p><p>I wanted to borrow that idea without dragging in a complete SIEM installation.</p><p>The first version would answer four questions:</p><ol><li>Which processes appeared or disappeared?</li><li>Which processes opened network connections?</li><li>Where were those connections going?</li><li>What changed between observations?</li></ol><p>There’s a fifth question, too, although it’s harder:</p><p>Should I actually care?</p><p>We’ll get to that one.</p><h3>Start with psutil</h3><p>Python’s standard library gives us access to plenty of operating-system functionality, but process inspection becomes much easier with psutil.</p><p>It’s a cross-platform library for examining running processes, system utilization, network connections, and other operating-system statistics.</p><p>On Linux, much of this information ultimately comes from kernel interfaces exposed through /proc.</p><p>Install it inside a virtual environment:</p><pre>mkdir process-watcher<br>cd process-watcher<br><br>python -m venv .venv<br>source .venv/bin/activate<br><br>python -m pip install psutil</pre><p>Create a file:</p><pre>nano watcher.py</pre><p>Before building the actual recorder, it’s worth seeing what the library exposes.</p><pre>import psutil<br><br>for process in psutil.process_iter(<br>    [&quot;pid&quot;, &quot;name&quot;, &quot;exe&quot;, &quot;username&quot;]<br>):<br>    try:<br>        info = process.info<br><br>        print(<br>            info[&quot;pid&quot;],<br>            info[&quot;name&quot;],<br>            info[&quot;exe&quot;],<br>            info[&quot;username&quot;]<br>        )<br><br>    except (<br>        psutil.NoSuchProcess,<br>        psutil.AccessDenied<br>    ):<br>        continue</pre><p>This is already more useful than it looks.</p><p>Every process has an identifier, but a PID alone isn’t a durable identity. Linux reuses process IDs. Process 4102 today isn’t necessarily the same process 4102 five minutes from now.</p><p>For a historical recorder, we need something better.</p><p>psutil.Process.create_time() gives us the process creation timestamp.</p><p>Combine that with the PID and we have a reasonably reliable identity for an observed process instance.</p><pre>identity = (<br>    process.pid,<br>    process.create_time()<br>)</pre><p>This distinction matters because otherwise our event history can quietly become fiction.</p><p>Imagine a browser process exits, then an unrelated utility receives the same PID. A naive monitor might decide that the browser transformed into a network utility.</p><p>Interesting theory. Terrible telemetry.</p><h3>Build a process snapshot</h3><p>The basic monitoring strategy is polling.</p><p>Every few seconds, we’ll inspect the system and compare the current state against the previous observation.</p><p>This isn’t the same as kernel-level event monitoring. A process can start and finish between polls without ever appearing in our results.</p><p>That’s an important limitation, not a minor implementation detail.</p><p>For now, though, polling is simple, understandable, and good enough to create a useful diagnostic history.</p><p>Here’s a snapshot function:</p><pre>import psutil<br><br>def get_process_snapshot():<br>    snapshot = {}<br><br>    fields = [<br>        &quot;pid&quot;,<br>        &quot;name&quot;,<br>        &quot;exe&quot;,<br>        &quot;username&quot;,<br>        &quot;create_time&quot;,<br>        &quot;cmdline&quot;<br>    ]<br><br>    for process in psutil.process_iter(<br>        fields,<br>        ad_value=None<br>    ):<br>        try:<br>            info = process.info<br><br>            pid = info[&quot;pid&quot;]<br>            created = info[&quot;create_time&quot;]<br><br>            if created is None:<br>                continue<br><br>            identity = (pid, created)<br><br>            snapshot[identity] = {<br>                &quot;pid&quot;: pid,<br>                &quot;created&quot;: created,<br>                &quot;name&quot;: info[&quot;name&quot;],<br>                &quot;exe&quot;: info[&quot;exe&quot;],<br>                &quot;username&quot;: info[&quot;username&quot;],<br>                &quot;cmdline&quot;: info[&quot;cmdline&quot;]<br>            }<br><br>        except (<br>            psutil.NoSuchProcess,<br>            psutil.AccessDenied,<br>            psutil.ZombieProcess<br>        ):<br>            continue<br><br>    return snapshot<br><br><br></pre><p>The result is a dictionary keyed by process identity.</p><p>Each entry contains metadata that can help us understand what we’re looking at.</p><p>The executable path is particularly useful.</p><p>A process called python3 doesn’t tell us very much. A Python interpreter running a script in a temporary directory is a different investigative lead from one running a known application in your home directory.</p><p>Neither is automatically malicious.</p><p>Context is doing the heavy lifting here.</p><h3>Detecting new and missing processes</h3><p>Once we have two snapshots, comparison is straightforward.</p><pre>previous = get_process_snapshot()<br><br># Later, after the polling interval:<br><br>current = get_process_snapshot()<br><br>started = current.keys() - previous.keys()<br>missing = previous.keys() - current.keys()<br><br>for identity in started:<br>    print(<br>        &quot;STARTED:&quot;,<br>        current[identity][&quot;name&quot;]<br>    )<br><br>for identity in missing:<br>    print(<br>        &quot;MISSING:&quot;,<br>        previous[identity][&quot;name&quot;]<br>    )</pre><p>Notice the word missing.</p><p>A process disappearing from one snapshot doesn’t necessarily prove that it terminated. Permissions, inspection failures, and transient collection problems can affect what the monitor sees.</p><p>In a proper event recorder, observations should be distinguished from conclusions.</p><p>That’s one of those boring engineering principles that suddenly becomes very interesting when you’re trying to determine whether something actually happened.</p><h3>Network connections are where things get interesting</h3><p>Processes are useful, but I was especially interested in outbound connections.</p><p>A machine can be doing very little visibly while still communicating with remote systems.</p><p>Software updates, browser synchronization, messaging applications, DNS requests, cloud services, and background telemetry can all generate network activity.</p><p>psutil.net_connections() exposes socket information, including local addresses, remote addresses, connection status, and process IDs when available.</p><p>Try:</p><pre>import psutil<br><br>connections = psutil.net_connections(<br>    kind=&quot;inet&quot;<br>)<br><br>for connection in connections:<br>    print(<br>        connection.pid,<br>        connection.laddr,<br>        connection.raddr,<br>        connection.status<br>    )</pre><p>On Linux, access to socket information is affected by permissions.</p><p>Without sufficient privileges, some connections may be missing from the results, or their owning processes may not be identifiable.</p><p>A blank PID does not mean that a connection has no owner.</p><p>It means the recorder couldn’t establish one.</p><p>That distinction is going to come up repeatedly.</p><h3>Why socket status matters</h3><p>TCP connections have states such as:</p><ul><li>ESTABLISHED</li><li>LISTEN</li><li>SYN_SENT</li><li>TIME_WAIT</li><li>CLOSE_WAIT</li></ul><p>A listening socket is not the same thing as an outbound connection.</p><p>A service listening on 127.0.0.1 may be exposing functionality only to local applications. A service listening on 0.0.0.0 is bound to all IPv4 interfaces, although firewall rules and network configuration still determine whether other machines can reach it.</p><p>Likewise, an established connection isn’t inherently suspicious.</p><p>A browser maintaining several encrypted connections is expected.</p><p>An unfamiliar executable maintaining a connection to an unfamiliar endpoint deserves investigation, but the endpoint alone won’t tell us why the connection exists.</p><p>We need to preserve the surrounding information.</p><h3>Give the recorder a memory</h3><p>Printing everything to the terminal works until the terminal closes.</p><p>I wanted an event history that could survive restarts and be queried afterward.</p><p>SQLite is a good fit.</p><p>It’s built into Python, doesn’t require a separate database server, and can comfortably handle a modest local event log.</p><p>We’ll store:</p><ul><li>The observation timestamp</li><li>The event category</li><li>The process PID</li><li>The process creation time</li><li>The executable name and path</li><li>The remote address</li><li>The remote port</li><li>Additional structured details</li></ul><p>I prefer keeping the event payload in JSON while preserving important fields as ordinary database columns.</p><p>That makes the history easy to inspect with SQL without forcing every possible event attribute into its own schema column.</p><h3>The complete recorder</h3><p>Here’s a functional baseline implementation.</p><p>It records process appearances and disappearances, tracks observed network socket changes, and stores everything in SQLite.</p><p>It also deliberately avoids logging command-line arguments by default. Those can contain access tokens, passwords, private filenames, and other information you probably don’t want sitting in an unencrypted diagnostic database.</p><pre><br>#!/usr/bin/env python3<br><br>import argparse<br>import json<br>import os<br>import signal<br>import sqlite3<br>import time<br>from datetime import datetime, timezone<br>from pathlib import Path<br><br>import psutil<br><br><br>DEFAULT_INTERVAL = 2.0<br><br>DEFAULT_DB = (<br>    Path.home()<br>    / &quot;.local&quot;<br>    / &quot;share&quot;<br>    / &quot;process-watcher&quot;<br>    / &quot;events.db&quot;<br>)<br><br>RUNNING = True<br><br><br>def utc_now():<br>    return datetime.now(<br>        timezone.utc<br>    ).isoformat(timespec=&quot;milliseconds&quot;)<br><br><br>def stop_handler(signum, frame):<br>    global RUNNING<br>    RUNNING = False<br><br><br>def initialize_database(path):<br>    path = Path(path).expanduser()<br><br>    path.parent.mkdir(<br>        parents=True,<br>        exist_ok=True,<br>        mode=0o700<br>    )<br><br>    connection = sqlite3.connect(path)<br><br>    connection.execute(<br>        &quot;&quot;&quot;<br>        CREATE TABLE IF NOT EXISTS events (<br>            id INTEGER PRIMARY KEY AUTOINCREMENT,<br>            timestamp TEXT NOT NULL,<br>            event_type TEXT NOT NULL,<br>            pid INTEGER,<br>            process_created REAL,<br>            process_name TEXT,<br>            executable TEXT,<br>            remote_address TEXT,<br>            remote_port INTEGER,<br>            details TEXT NOT NULL<br>        )<br>        &quot;&quot;&quot;<br>    )<br><br>    connection.execute(<br>        &quot;&quot;&quot;<br>        CREATE INDEX IF NOT EXISTS<br>        idx_events_timestamp<br>        ON events(timestamp)<br>        &quot;&quot;&quot;<br>    )<br><br>    connection.execute(<br>        &quot;&quot;&quot;<br>        CREATE INDEX IF NOT EXISTS<br>        idx_events_type<br>        ON events(event_type)<br>        &quot;&quot;&quot;<br>    )<br><br>    connection.commit()<br><br>    try:<br>        os.chmod(path, 0o600)<br>    except OSError:<br>        pass<br><br>    return connection<br><br><br>def record_event(<br>    db,<br>    event_type,<br>    process=None,<br>    remote_address=None,<br>    remote_port=None,<br>    details=None<br>):<br>    process = process or {}<br><br>    payload = json.dumps(<br>        details or {},<br>        sort_keys=True,<br>        default=str<br>    )<br><br>    db.execute(<br>        &quot;&quot;&quot;<br>        INSERT INTO events (<br>            timestamp,<br>            event_type,<br>            pid,<br>            process_created,<br>            process_name,<br>            executable,<br>            remote_address,<br>            remote_port,<br>            details<br>        )<br>        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)<br>        &quot;&quot;&quot;,<br>        (<br>            utc_now(),<br>            event_type,<br>            process.get(&quot;pid&quot;),<br>            process.get(&quot;created&quot;),<br>            process.get(&quot;name&quot;),<br>            process.get(&quot;exe&quot;),<br>            remote_address,<br>            remote_port,<br>            payload<br>        )<br>    )<br><br><br>def process_snapshot():<br>    result = {}<br><br>    fields = [<br>        &quot;pid&quot;,<br>        &quot;name&quot;,<br>        &quot;exe&quot;,<br>        &quot;username&quot;,<br>        &quot;create_time&quot;<br>    ]<br><br>    for proc in psutil.process_iter(<br>        fields,<br>        ad_value=None<br>    ):<br>        try:<br>            info = proc.info<br><br>            pid = info.get(&quot;pid&quot;)<br>            created = info.get(&quot;create_time&quot;)<br><br>            if pid is None or created is None:<br>                continue<br><br>            identity = (pid, created)<br><br>            result[identity] = {<br>                &quot;pid&quot;: pid,<br>                &quot;created&quot;: created,<br>                &quot;name&quot;: info.get(&quot;name&quot;),<br>                &quot;exe&quot;: info.get(&quot;exe&quot;),<br>                &quot;username&quot;: info.get(&quot;username&quot;)<br>            }<br><br>        except (<br>            psutil.NoSuchProcess,<br>            psutil.AccessDenied,<br>            psutil.ZombieProcess<br>        ):<br>            continue<br><br>    return result<br><br><br>def address_parts(address):<br>    if not address:<br>        return None, None<br><br>    return address.ip, address.port<br><br><br>def connection_snapshot(processes):<br>    result = {}<br><br>    try:<br>        connections = psutil.net_connections(<br>            kind=&quot;inet&quot;<br>        )<br><br>    except (psutil.AccessDenied, OSError) as exc:<br>        raise RuntimeError(<br>            f&quot;Unable to inspect connections: {exc}&quot;<br>        ) from exc<br><br>    pid_lookup = {<br>        info[&quot;pid&quot;]: info<br>        for info in processes.values()<br>    }<br><br>    for conn in connections:<br>        local_ip, local_port = address_parts(<br>            conn.laddr<br>        )<br><br>        remote_ip, remote_port = address_parts(<br>            conn.raddr<br>        )<br><br>        process = pid_lookup.get(<br>            conn.pid,<br>            {}<br>        )<br><br>        # The socket is unidentified if the<br>        # owning process could not be resolved.<br>        created = process.get(&quot;created&quot;)<br><br>        key = (<br>            conn.pid,<br>            created,<br>            conn.type,<br>            local_ip,<br>            local_port,<br>            remote_ip,<br>            remote_port,<br>            conn.status<br>        )<br><br>        result[key] = {<br>            &quot;pid&quot;: conn.pid,<br>            &quot;process&quot;: process,<br>            &quot;local_address&quot;: local_ip,<br>            &quot;local_port&quot;: local_port,<br>            &quot;remote_address&quot;: remote_ip,<br>            &quot;remote_port&quot;: remote_port,<br>            &quot;status&quot;: conn.status,<br>            &quot;socket_type&quot;: conn.type<br>        }<br><br>    return result<br><br><br>def describe_event(event_type, info):<br>    name = info.get(&quot;name&quot;) or &quot;unknown&quot;<br>    pid = info.get(&quot;pid&quot;)<br><br>    print(<br>        f&quot;[{utc_now()}] &quot;<br>        f&quot;{event_type:&lt;18} &quot;<br>        f&quot;pid={pid} &quot;<br>        f&quot;name={name}&quot;,<br>        flush=True<br>    )<br><br><br>def monitor(db, interval):<br>    previous_processes = None<br>    previous_connections = None<br><br>    while RUNNING:<br>        try:<br>            processes = process_snapshot()<br>            connections = connection_snapshot(<br>                processes<br>            )<br><br>        except Exception as exc:<br>            print(<br>                f&quot;[{utc_now()}] &quot;<br>                f&quot;COLLECTION_ERROR: {exc}&quot;,<br>                flush=True<br>            )<br><br>            time.sleep(interval)<br>            continue<br><br>        if previous_processes is not None:<br>            new_ids = (<br>                processes.keys()<br>                - previous_processes.keys()<br>            )<br><br>            missing_ids = (<br>                previous_processes.keys()<br>                - processes.keys()<br>            )<br><br>            for identity in new_ids:<br>                info = processes[identity]<br><br>                record_event(<br>                    db,<br>                    &quot;PROCESS_OBSERVED&quot;,<br>                    process=info<br>                )<br><br>                describe_event(<br>                    &quot;PROCESS_OBSERVED&quot;,<br>                    info<br>                )<br><br>            for identity in missing_ids:<br>                info = previous_processes[identity]<br><br>                record_event(<br>                    db,<br>                    &quot;PROCESS_MISSING&quot;,<br>                    process=info<br>                )<br><br>                describe_event(<br>                    &quot;PROCESS_MISSING&quot;,<br>                    info<br>                )<br><br>        if previous_connections is not None:<br>            new_connections = (<br>                connections.keys()<br>                - previous_connections.keys()<br>            )<br><br>            missing_connections = (<br>                previous_connections.keys()<br>                - connections.keys()<br>            )<br><br>            for key in new_connections:<br>                info = connections[key]<br><br>                record_event(<br>                    db,<br>                    &quot;SOCKET_OBSERVED&quot;,<br>                    process=info[&quot;process&quot;],<br>                    remote_address=info[<br>                        &quot;remote_address&quot;<br>                    ],<br>                    remote_port=info[<br>                        &quot;remote_port&quot;<br>                    ],<br>                    details={<br>                        &quot;local_address&quot;: info[<br>                            &quot;local_address&quot;<br>                        ],<br>                        &quot;local_port&quot;: info[<br>                            &quot;local_port&quot;<br>                        ],<br>                        &quot;status&quot;: info[&quot;status&quot;],<br>                        &quot;socket_type&quot;: info[<br>                            &quot;socket_type&quot;<br>                        ]<br>                    }<br>                )<br><br>            for key in missing_connections:<br>                info = previous_connections[key]<br><br>                record_event(<br>                    db,<br>                    &quot;SOCKET_MISSING&quot;,<br>                    process=info[&quot;process&quot;],<br>                    remote_address=info[<br>                        &quot;remote_address&quot;<br>                    ],<br>                    remote_port=info[<br>                        &quot;remote_port&quot;<br>                    ],<br>                    details={<br>                        &quot;local_address&quot;: info[<br>                            &quot;local_address&quot;<br>                        ],<br>                        &quot;local_port&quot;: info[<br>                            &quot;local_port&quot;<br>                        ],<br>                        &quot;status&quot;: info[&quot;status&quot;]<br>                    }<br>                )<br><br>        previous_processes = processes<br>        previous_connections = connections<br><br>        db.commit()<br><br>        time.sleep(interval)<br><br><br>def main():<br>    parser = argparse.ArgumentParser(<br>        description=(<br>            &quot;Local process and network &quot;<br>            &quot;activity recorder&quot;<br>        )<br>    )<br><br>    parser.add_argument(<br>        &quot;--interval&quot;,<br>        type=float,<br>        default=DEFAULT_INTERVAL<br>    )<br><br>    parser.add_argument(<br>        &quot;--database&quot;,<br>        default=str(DEFAULT_DB)<br>    )<br><br>    args = parser.parse_args()<br><br>    if args.interval &lt;= 0:<br>        parser.error(<br>            &quot;--interval must be positive&quot;<br>        )<br><br>    signal.signal(<br>        signal.SIGINT,<br>        stop_handler<br>    )<br><br>    signal.signal(<br>        signal.SIGTERM,<br>        stop_handler<br>    )<br><br>    db = initialize_database(<br>        args.database<br>    )<br><br>    print(<br>        f&quot;Recording to {args.database}&quot;<br>    )<br><br>    print(<br>        f&quot;Polling every {args.interval}s&quot;<br>    )<br><br>    print(<br>        &quot;Press Ctrl+C to stop.&quot;<br>    )<br><br>    try:<br>        monitor(<br>            db,<br>            args.interval<br>        )<br><br>    finally:<br>        db.commit()<br>        db.close()<br><br><br>if __name__ == &quot;__main__&quot;:<br>    main()<br></pre><p>There are a few deliberate design choices here.</p><p>The initial snapshot establishes a baseline rather than recording every existing process as newly started.</p><p>The database stores observation timestamps in UTC. This makes records easier to correlate with other logs across time zones.</p><p>Socket identity includes the local endpoint, remote endpoint, protocol type, process identity where available, and connection status. A change in TCP state can therefore produce a missing-socket event followed by an observed-socket event.</p><p>That doesn’t mean a new physical connection was necessarily created.</p><p>Also, psutil does not expose a stable socket identifier through this API. A reused endpoint tuple can represent a different socket. UDP observations are even less expressive because they don’t have TCP-style connection states.</p><p>This is a polling-based activity recorder, not a perfect socket lifecycle tracer.</p><p>And while the database permissions reduce accidental exposure, they don’t protect against another process running with the same user privileges. The monitor is intended for a trusted personal machine, not as a tamper-resistant forensic appliance.</p><h3>Running the monitor</h3><p>Start it with:</p><pre><br>python watcher.py<br></pre><p>Or change the polling interval:</p><pre><br>python watcher.py --interval 1<br></pre><p>A shorter interval increases the chance of observing short-lived activity, but it also increases collection overhead.</p><p>Scanning system-wide network connections can be relatively expensive, particularly on machines with large numbers of sockets.</p><p>I wouldn’t default to polling ten times per second just because the option exists.</p><p>For ordinary desktop diagnostics, one or two seconds is a reasonable starting point. Measure the overhead on your own system before deciding what interval makes sense.</p><p>On Linux, you can check the monitor itself with:</p><pre><br>ps -o pid,%cpu,%mem,etime,cmd -C python<br></pre><p>If you need visibility into sockets belonging to other users, additional privileges may be required. Run the tool unprivileged first and understand the gaps before considering elevated execution.</p><h3>Query the history</h3><p>The recorder writes events into:</p><pre><br>~/.local/share/process-watcher/events.db<br></pre><p>If the SQLite command-line client is installed, inspect the database:</p><pre><br>sqlite3 ~/.local/share/process-watcher/events.db<br></pre><p>List the most recent events:</p><pre><br>SELECT<br>    timestamp,<br>    event_type,<br>    pid,<br>    process_name<br>FROM events<br>ORDER BY id DESC<br>LIMIT 30;<br></pre><p>Find remote endpoints:</p><pre><br>SELECT<br>    timestamp,<br>    process_name,<br>    remote_address,<br>    remote_port<br>FROM events<br>WHERE event_type = &#39;SOCKET_OBSERVED&#39;<br>  AND remote_address IS NOT NULL<br>ORDER BY id DESC<br>LIMIT 50;<br></pre><p>Find the most frequently observed remote endpoints:</p><pre><br>SELECT<br>    remote_address,<br>    remote_port,<br>    COUNT(*) AS observations<br>FROM events<br>WHERE event_type = &#39;SOCKET_OBSERVED&#39;<br>  AND remote_address IS NOT NULL<br>GROUP BY<br>    remote_address,<br>    remote_port<br>ORDER BY observations DESC<br>LIMIT 20;<br></pre><p>That last query is useful, but be careful interpreting it.</p><p>The count represents observed socket-state appearances, not connection attempts, packets, bytes transferred, or unique communications.</p><p>A noisy process can generate many records without doing anything dangerous.</p><p>A malicious process might generate one record, or none if it runs entirely between polling intervals.</p><p>Numbers are very good at looking authoritative while quietly measuring something other than what you think they’re measuring.</p><p><strong>So what does suspicious activity actually look like?</strong></p><p>This is where the project stops being a straightforward Python exercise.</p><p>A process connecting to an unfamiliar IP address is not automatically suspicious.</p><p>Neither is an executable running from a temporary directory.</p><p>Neither is a background process starting without a visible application window.</p><p>Linux desktops contain plenty of software that behaves in ways that look strange until you understand its purpose.</p><p>Package managers create temporary files. Browsers launch sandboxed helper processes. Desktop services communicate over local sockets. Updaters contact remote servers. Applications use content delivery networks whose IP addresses change.</p><p>Even ordinary Python tooling can look questionable.</p><p>A development server might listen on a network interface. A test suite might create dozens of short-lived processes. A package installation might contact several different domains.</p><p>You could build an alert system that treats all of these behaviors as suspicious.</p><p>It would be spectacularly annoying.</p><h3>The better approach: investigate combinations</h3><p><strong>Consider a hypothetical sequence:</strong></p><p>A new executable appears in an unexpected directory.</p><p>The executable has no obvious relationship to the application you were using.</p><p>It starts repeatedly.</p><p>Each time it runs, it establishes an outbound connection to the same remote endpoint.</p><p>Its parent process is unexpected.</p><p>That combination is more interesting than any individual event.</p><p>But our current recorder doesn’t capture parent-process relationships or executable hashes. It also doesn’t prove that a particular process initiated a connection, only that the socket was associated with it during an observation.</p><p>So we’d need to collect more evidence before making a claim.</p><p>This is why I dislike security dashboards that reduce everything to a red warning icon and a vaguely threatening number.</p><p>The important part is the evidence behind the warning.</p><h3>Add executable hashing</h3><p>One useful extension is recording the SHA-256 hash of executable files.</p><p>That gives us a way to distinguish binaries even when their filenames are identical.</p><pre><br>import hashlib<br>from pathlib import Path<br><br><br>def sha256_file(path):<br>    digest = hashlib.sha256()<br><br>    try:<br>        with Path(path).open(&quot;rb&quot;) as file:<br>            while True:<br>                chunk = file.read(<br>                    1024 * 1024<br>                )<br><br>                if not chunk:<br>                    break<br><br>                digest.update(chunk)<br><br>    except (OSError, TypeError):<br>        return None<br><br>    return digest.hexdigest()<br></pre><p>You could call this when a previously unseen executable path appears, rather than repeatedly hashing every running process.</p><p>Hashing has costs, and the file on disk might have changed since the process started. A hash of the current path isn’t necessarily a hash of the exact executable image currently mapped into memory.</p><p>Still, it’s useful metadata.</p><p>It can help answer whether two suspicious-looking events involved the same binary, or whether an executable at a familiar path has changed.</p><p>For a more complete implementation, I’d also record parent PID, parent creation time, file ownership, and the result of any executable-path inspection.</p><h3>The thing this tool cannot see</h3><p>There’s a major blind spot in our design.</p><p>Polling isn’t event tracing.</p><p>A process can start, make a connection, and disappear between two snapshots.</p><p>Likewise, a TCP connection can be established and closed before the next poll.</p><p>You can shorten the interval, but you cannot eliminate this limitation through polling alone.</p><p>On Linux, more complete approaches include kernel audit facilities, eBPF instrumentation, and other event-based tracing mechanisms.</p><p>For example, eBPF programs can attach to selected kernel tracepoints or hooks and report events as they occur.</p><p>That opens the door to a more capable recorder, although it introduces kernel-version differences, privilege requirements, event-buffer management, and considerably more complicated code.</p><p>There’s also a distinction between observing a network connection and observing what data was transmitted.</p><p>Our Python recorder captures socket metadata.</p><p>It does not capture packet payloads.</p><p>It cannot tell whether a connection transmitted a password, downloaded a software update, or exchanged a few harmless bytes.</p><p>A destination address and port are not a transcript.</p><p>That’s a fairly important distinction if you’re going to use this kind of information for security investigations.</p><h3>Turning observations into useful alerts</h3><p>The next logical step is a rules engine.</p><p>But I wouldn’t begin with an enormous collection of supposedly suspicious indicators downloaded from somewhere.</p><p>I’d start with local baselines.</p><p>Record activity for a while. Identify familiar applications, recurring services, expected endpoints, and normal startup patterns.</p><p>Then look for changes.</p><p><strong>Examples of useful investigative rules might include:</strong></p><p>New executable in an unusual location: A previously unseen binary appears under a temporary or user-writable directory and starts making network connections.</p><p>Unexpected listener: A process begins listening on a non-loopback address where no corresponding service was previously observed.</p><p>Repeated unfamiliar destination: The same process repeatedly appears with connections to an endpoint that hasn’t been part of the machine’s normal activity.</p><p>Changed executable: A familiar executable path now has a different file hash, without an obvious software update.</p><p>These should produce investigation candidates, not automatic declarations of compromise.</p><p>The difference matters.</p><p>If a monitoring tool confidently tells you that ordinary behavior is malicious, eventually you’ll either stop trusting it or start breaking things to satisfy it.</p><p>Neither outcome is particularly useful.</p><h3>A privacy tool that creates its own privacy problem</h3><p>There’s a slightly uncomfortable detail here.</p><p>We’re collecting a history of application behavior.</p><p>That history can reveal quite a lot about the person using the computer.</p><p>Even without packet contents, network metadata can expose patterns involving work schedules, software usage, communications, and browsing activity.</p><p>Executable paths may contain usernames or project names.</p><p>Command-line arguments can contain secrets.</p><p>A local monitoring database is itself sensitive information.</p><p>So if I were extending this into a tool intended for long-term use, I’d add a retention policy, database size limits, configurable exclusions, and a clear option to disable collection of particular fields.</p><p>For example, SQLite makes it easy to delete old events:</p><pre><br>DELETE FROM events<br>WHERE timestamp &lt; datetime(<br>    &#39;now&#39;,<br>    &#39;-7 days&#39;<br>);<br></pre><p>However, our timestamps use ISO 8601 formatting with a T separator, while SQLite’s default datetime() output uses a space.</p><p>For reliable comparisons, use a cutoff generated in Python with the same timestamp format as the stored records:</p><pre><br>from datetime import (<br>    datetime,<br>    timedelta,<br>    timezone<br>)<br><br>cutoff = (<br>    datetime.now(timezone.utc)<br>    - timedelta(days=7)<br>).isoformat(timespec=&quot;milliseconds&quot;)<br><br>db.execute(<br>    &quot;DELETE FROM events WHERE timestamp &lt; ?&quot;,<br>    (cutoff,)<br>)<br><br>db.commit()<br></pre><p>Deletion also doesn’t guarantee immediate removal of historical bytes from the database file or associated storage.</p><p>SQLite may retain freed pages for reuse, and filesystem backups or snapshots may preserve older copies.</p><p>For a privacy-conscious deployment, retention should be designed from the beginning rather than bolted on after the database has accumulated months of activity.</p><p>A tool that watches everything can become a very convenient archive for someone else.</p><p>Wonderful. We invented another problem.</p><h3>What I’d build next</h3><p>The baseline recorder is intentionally small enough to understand.</p><p>It can tell us what processes and sockets were visible at each polling interval and preserve changes for later inspection.</p><p>That’s useful, but there’s room to make it considerably better.</p><p>I’d extend it with a few specific capabilities.</p><p>First, a proper process ancestry model. Knowing that a process exists is less useful than knowing which process launched it, when, and under what executable identity.</p><p>Second, DNS correlation. An IP address is often a poor investigative clue by itself. Capturing relevant DNS observations would help connect some network activity to hostnames, while recognizing that DNS caching, encrypted DNS, shared addresses, and application-level resolution complicate attribution.</p><p>Third, an event-based backend for Linux. The polling implementation could remain available as a portable fallback, while a kernel-supported event source captures short-lived process and network activity more reliably.</p><p>Fourth, a local HTML report generator.</p><p>Not another dashboard running seventeen JavaScript frameworks.</p><p>An actual report: timestamped events, executable identities, remote endpoints, and a timeline that can be opened in a browser without requiring an account.</p><p>I’d also want a baseline comparison mode so I could record an ordinary desktop session, install a new application, record another session, and examine the differences.</p><p>That would be useful for privacy investigations, software testing, and understanding unfamiliar applications.</p><p>It would also be a considerably more honest experiment than installing a random network monitor and declaring every unfamiliar connection evidence of espionage.</p><h3>The strange comfort of knowing what’s running</h3><p>The thing I like about this project is that it replaces vague suspicion with observable data.</p><p>Computers are noisy.</p><p>Modern desktop software is complicated. Applications launch helper processes, maintain background connections, schedule work, and communicate with services you might not recognize.</p><p>Most of that behavior is entirely legitimate.</p><p>Some of it is unnecessary.</p><p>Occasionally, something genuinely deserves closer attention.</p><p>The difficult part isn’t collecting a list of processes or remote addresses. Python makes that relatively easy.</p><p>The difficult part is preserving enough context to understand what the observations mean.</p><p>That’s where a simple recorder becomes an actual engineering project.</p><p>And there’s something satisfying about building your own instruments instead of trusting a dashboard whose detection logic you can’t inspect.</p><p>The code isn’t magic. It doesn’t identify malware, inspect encrypted traffic, or guarantee complete visibility into the operating system.</p><p>It watches what it can see, records changes, and gives you something concrete to investigate.</p><p>For a few hundred lines of Python and a SQLite database, that’s a decent start.</p><p>⸻</p><p>If you want to take this approach further, my <a href="https://numbpilled.gumroad.com/l/pythonninja">Python Automation Ninja toolkit </a>focuses on building practical Python utilities, automation workflows, and reusable tooling. The point is to own the machinery, understand how it works, and stop treating every useful system capability as something that requires another subscription.</p><ul><li><a href="https://numbpilled.gumroad.com/l/pythonninja">Python Automation Ninja: 10 Custom Python Automations</a></li><li><a href="https://numbpilled.gumroad.com/l/ultimate-python-bash-pack">SHELL SHOCKED: The Underground Megapack of Python and Bash Scripts - 8 Guides, No GUI, All Power</a></li></ul><p>The monitoring code in this article is a standalone starting point. You can modify it, extend the event model, or replace the polling backend entirely.</p><p>Your computer has been doing things without asking you for years. You might as well start keeping receipts.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7419978911c9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/code-is-data-data-is-code/i-built-a-python-tool-that-watches-what-my-computer-does-behind-my-back-7419978911c9">I Built a Python Tool That Watches What My Computer Does Behind My Back</a> was originally published in <a href="https://medium.com/code-is-data-data-is-code">Code is Data, Data is Code</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Common Lisp Macros And Python Decorators]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/code-is-data-data-is-code/common-lisp-macros-and-python-decorators-193fe694344a?source=rss----c7439eb7d33b---4"><img src="https://cdn-images-1.medium.com/max/2600/0*q00cwep_hcM_qmoo" width="16384"></a></p><p class="medium-feed-snippet">Some Thoughts About Decorators as Replacement for Lisp Macros</p><p class="medium-feed-link"><a href="https://medium.com/code-is-data-data-is-code/common-lisp-macros-and-python-decorators-193fe694344a?source=rss----c7439eb7d33b---4">Continue reading on Code is Data, Data is Code »</a></p></div>]]></description>
            <link>https://medium.com/code-is-data-data-is-code/common-lisp-macros-and-python-decorators-193fe694344a?source=rss----c7439eb7d33b---4</link>
            <guid isPermaLink="false">https://medium.com/p/193fe694344a</guid>
            <category><![CDATA[python-programming]]></category>
            <category><![CDATA[environment-variables]]></category>
            <category><![CDATA[functional-programming]]></category>
            <category><![CDATA[common-lisp]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Gwang-Jin]]></dc:creator>
            <pubDate>Wed, 23 Sep 2026 05:51:26 GMT</pubDate>
            <atom:updated>2026-09-23T05:51:27.816Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Forth and Lisp: Two Very Different Languages With the Same Philosophy]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/code-is-data-data-is-code/forth-and-lisp-two-very-different-languages-with-the-same-philosophy-558c7fde7bc4?source=rss----c7439eb7d33b---4"><img src="https://cdn-images-1.medium.com/max/2600/0*yWioI0sBa4wimK_b" width="4162"></a></p><p class="medium-feed-snippet">What Programming Language Should You Learn Next?</p><p class="medium-feed-link"><a href="https://medium.com/code-is-data-data-is-code/forth-and-lisp-two-very-different-languages-with-the-same-philosophy-558c7fde7bc4?source=rss----c7439eb7d33b---4">Continue reading on Code is Data, Data is Code »</a></p></div>]]></description>
            <link>https://medium.com/code-is-data-data-is-code/forth-and-lisp-two-very-different-languages-with-the-same-philosophy-558c7fde7bc4?source=rss----c7439eb7d33b---4</link>
            <guid isPermaLink="false">https://medium.com/p/558c7fde7bc4</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[forth]]></category>
            <category><![CDATA[lisp]]></category>
            <category><![CDATA[programming-languages]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Aleena]]></dc:creator>
            <pubDate>Fri, 24 Jul 2026 08:54:15 GMT</pubDate>
            <atom:updated>2026-07-24T08:54:14.456Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Code Is Data and Data Is Code: An Invitation to Write About Programming Ideas]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/code-is-data-data-is-code/code-is-data-and-data-is-code-an-invitation-to-write-differently-about-programming-f3e4496c7bb5?source=rss----c7439eb7d33b---4"><img src="https://cdn-images-1.medium.com/max/2600/0*t_P_DeY4Lk11hY-V" width="3503"></a></p><p class="medium-feed-snippet">A home for Lisp, functional programming, expressive languages, unusual tools, and the small ideas that permanently change how we write code</p><p class="medium-feed-link"><a href="https://medium.com/code-is-data-data-is-code/code-is-data-and-data-is-code-an-invitation-to-write-differently-about-programming-f3e4496c7bb5?source=rss----c7439eb7d33b---4">Continue reading on Code is Data, Data is Code »</a></p></div>]]></description>
            <link>https://medium.com/code-is-data-data-is-code/code-is-data-and-data-is-code-an-invitation-to-write-differently-about-programming-f3e4496c7bb5?source=rss----c7439eb7d33b---4</link>
            <guid isPermaLink="false">https://medium.com/p/f3e4496c7bb5</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[functional-programming]]></category>
            <category><![CDATA[programming-paradigms]]></category>
            <category><![CDATA[lisp]]></category>
            <category><![CDATA[programming-languages]]></category>
            <dc:creator><![CDATA[Gwang-Jin]]></dc:creator>
            <pubDate>Sun, 19 Jul 2026 21:06:02 GMT</pubDate>
            <atom:updated>2026-07-20T00:25:12.000Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Common Lisp Became Easier to Set Up in 2026]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/code-is-data-data-is-code/common-lisp-became-easier-to-setup-in-2026-89119624a3a5?source=rss----c7439eb7d33b---4"><img src="https://cdn-images-1.medium.com/max/2600/0*saeuhDGZVqEBWsAs" width="5000"></a></p><p class="medium-feed-snippet">A modern set up now takes much less time, and we have now much more options</p><p class="medium-feed-link"><a href="https://medium.com/code-is-data-data-is-code/common-lisp-became-easier-to-setup-in-2026-89119624a3a5?source=rss----c7439eb7d33b---4">Continue reading on Code is Data, Data is Code »</a></p></div>]]></description>
            <link>https://medium.com/code-is-data-data-is-code/common-lisp-became-easier-to-setup-in-2026-89119624a3a5?source=rss----c7439eb7d33b---4</link>
            <guid isPermaLink="false">https://medium.com/p/89119624a3a5</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[lisp]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[software-testing]]></category>
            <category><![CDATA[common-lisp]]></category>
            <dc:creator><![CDATA[Gwang-Jin]]></dc:creator>
            <pubDate>Sat, 18 Jul 2026 16:09:07 GMT</pubDate>
            <atom:updated>2026-07-18T16:19:06.495Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Most People Get the OOP vs FP Debate Wrong — Because They Never Met Real Functional Programming]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/code-is-data-data-is-code/most-people-get-the-oop-vs-fp-debate-wrong-because-they-never-met-real-functional-programming-50b82e62c669?source=rss----c7439eb7d33b---4"><img src="https://cdn-images-1.medium.com/max/2600/0*Nfsuk1xPMpM9zvhc" width="4075"></a></p><p class="medium-feed-snippet">Multiple Dispatch, Closures, and what Lisp programmers settled 40 years ago</p><p class="medium-feed-link"><a href="https://medium.com/code-is-data-data-is-code/most-people-get-the-oop-vs-fp-debate-wrong-because-they-never-met-real-functional-programming-50b82e62c669?source=rss----c7439eb7d33b---4">Continue reading on Code is Data, Data is Code »</a></p></div>]]></description>
            <link>https://medium.com/code-is-data-data-is-code/most-people-get-the-oop-vs-fp-debate-wrong-because-they-never-met-real-functional-programming-50b82e62c669?source=rss----c7439eb7d33b---4</link>
            <guid isPermaLink="false">https://medium.com/p/50b82e62c669</guid>
            <category><![CDATA[oop]]></category>
            <category><![CDATA[common-lisp]]></category>
            <category><![CDATA[polymorphism]]></category>
            <category><![CDATA[functional-programming]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Gwang-Jin]]></dc:creator>
            <pubDate>Fri, 06 Mar 2026 10:16:28 GMT</pubDate>
            <atom:updated>2026-04-23T22:46:41.624Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Common Lisp for Data Scientists: A Field Report From My “Tidyverse Weekend”]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/code-is-data-data-is-code/common-lisp-for-data-scientists-a-field-report-from-my-tidyverse-weekend-powered-by-e000cfd0c562?source=rss----c7439eb7d33b---4"><img src="https://cdn-images-1.medium.com/max/2600/0*A4zUEAOYOj3D7-Cq" width="3000"></a></p><p class="medium-feed-snippet">Rebuilding Tidyverse with Common Lisp us</p><p class="medium-feed-link"><a href="https://medium.com/code-is-data-data-is-code/common-lisp-for-data-scientists-a-field-report-from-my-tidyverse-weekend-powered-by-e000cfd0c562?source=rss----c7439eb7d33b---4">Continue reading on Code is Data, Data is Code »</a></p></div>]]></description>
            <link>https://medium.com/code-is-data-data-is-code/common-lisp-for-data-scientists-a-field-report-from-my-tidyverse-weekend-powered-by-e000cfd0c562?source=rss----c7439eb7d33b---4</link>
            <guid isPermaLink="false">https://medium.com/p/e000cfd0c562</guid>
            <category><![CDATA[vibe-coding]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[tidyverse]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[r-programming]]></category>
            <dc:creator><![CDATA[Gwang-Jin]]></dc:creator>
            <pubDate>Sat, 24 Jan 2026 17:27:55 GMT</pubDate>
            <atom:updated>2026-01-24T17:27:53.952Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[A Modern Guide to Common Lisp Package Management]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/code-is-data-data-is-code/a-modern-guide-to-common-lisp-package-management-381454eee11f?source=rss----c7439eb7d33b---4"><img src="https://cdn-images-1.medium.com/max/2600/0*SQE-5_EgkmE9tZdk" width="9504"></a></p><p class="medium-feed-snippet">Quicklisp, Roswell, and Ultralisp-when to use each, and how to avoid the common setup traps</p><p class="medium-feed-link"><a href="https://medium.com/code-is-data-data-is-code/a-modern-guide-to-common-lisp-package-management-381454eee11f?source=rss----c7439eb7d33b---4">Continue reading on Code is Data, Data is Code »</a></p></div>]]></description>
            <link>https://medium.com/code-is-data-data-is-code/a-modern-guide-to-common-lisp-package-management-381454eee11f?source=rss----c7439eb7d33b---4</link>
            <guid isPermaLink="false">https://medium.com/p/381454eee11f</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[installation]]></category>
            <category><![CDATA[lisp]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[common-lisp]]></category>
            <dc:creator><![CDATA[Gwang-Jin]]></dc:creator>
            <pubDate>Thu, 15 Jan 2026 12:27:31 GMT</pubDate>
            <atom:updated>2026-07-22T11:02:57.585Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[That Time Doom-Modeline Spoke Chinese to Me]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/code-is-data-data-is-code/that-time-doom-modeline-spoke-chinese-to-me-80de6725b10b?source=rss----c7439eb7d33b---4"><img src="https://cdn-images-1.medium.com/max/2600/0*-oCbfna5eyVnfswf" width="6016"></a></p><p class="medium-feed-snippet">Your Modeline Isn&#x2019;t Multilingual, It&#x2019;s Just Missing a Font</p><p class="medium-feed-link"><a href="https://medium.com/code-is-data-data-is-code/that-time-doom-modeline-spoke-chinese-to-me-80de6725b10b?source=rss----c7439eb7d33b---4">Continue reading on Code is Data, Data is Code »</a></p></div>]]></description>
            <link>https://medium.com/code-is-data-data-is-code/that-time-doom-modeline-spoke-chinese-to-me-80de6725b10b?source=rss----c7439eb7d33b---4</link>
            <guid isPermaLink="false">https://medium.com/p/80de6725b10b</guid>
            <category><![CDATA[emacs-lisp]]></category>
            <category><![CDATA[lisp]]></category>
            <category><![CDATA[emacs]]></category>
            <category><![CDATA[emacs-konfigürasyon]]></category>
            <category><![CDATA[doom-emacs]]></category>
            <dc:creator><![CDATA[Gwang-Jin]]></dc:creator>
            <pubDate>Fri, 22 Aug 2025 10:07:54 GMT</pubDate>
            <atom:updated>2025-08-22T10:14:02.095Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Making Macroexpansion Visible in Common Lisp]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/code-is-data-data-is-code/giving-macros-a-voice-in-common-lisp-d13f117ea0ca?source=rss----c7439eb7d33b---4"><img src="https://cdn-images-1.medium.com/max/1024/1*H4vo6WDooO2Oupe0kpegeg.jpeg" width="1024"></a></p><p class="medium-feed-snippet">The Power of Hooks</p><p class="medium-feed-link"><a href="https://medium.com/code-is-data-data-is-code/giving-macros-a-voice-in-common-lisp-d13f117ea0ca?source=rss----c7439eb7d33b---4">Continue reading on Code is Data, Data is Code »</a></p></div>]]></description>
            <link>https://medium.com/code-is-data-data-is-code/giving-macros-a-voice-in-common-lisp-d13f117ea0ca?source=rss----c7439eb7d33b---4</link>
            <guid isPermaLink="false">https://medium.com/p/d13f117ea0ca</guid>
            <category><![CDATA[common-lisp]]></category>
            <category><![CDATA[hooks]]></category>
            <category><![CDATA[lisp]]></category>
            <category><![CDATA[macro]]></category>
            <category><![CDATA[debugging]]></category>
            <dc:creator><![CDATA[Gwang-Jin]]></dc:creator>
            <pubDate>Thu, 03 Apr 2025 23:01:43 GMT</pubDate>
            <atom:updated>2026-05-07T15:46:03.069Z</atom:updated>
        </item>
    </channel>
</rss>