DEV Community

Cover image for Poltergeist: A ghost in your Mac's menu bar
Aravind Sathesh
Aravind Sathesh

Posted on

Poltergeist: A ghost in your Mac's menu bar

Hacktoberfest Open-Source AI Challenge: Ghost in the Machine Submission 👻🌼

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

What I Built

I built Poltergeist, a macOS menu bar app that acts like a ghost living inside your computer. It notices what you're doing and occasionally whispers something about it.

The idea was to take Ghost in the Machine literally. Instead of opening a chatbot and asking it questions, I wanted something that quietly lives on the Mac, notices little things, and speaks up when you don't expect it.

Poltergeist watches shell commands, app usage, idle time, battery levels, and late-night activity. When something catches its attention, a local Gemma model turns it into a short, context-aware whisper.

For example, running brew upgrade might prompt a remark about the machine's previous owner neglecting updates. Spending three hours in Xcode might earn you a comment about someone who once stayed up all night working on the same machine.

The whispers appear just below the menu bar. They fade in, type themselves out, linger for a moment, and disappear. The ghost icon flickers when it speaks.

I wanted the writing to feel a little unsettling without being completely random. A force-push, a nearly empty battery, or repeatedly running the same command should produce a different reaction from simply spending an hour in your editor.

Clicking the ghost opens a panel with your three most recent whispers and a button to summon another. You can choose between three haunting levels:

  • Faint: A whisper about every 30 minutes.
  • Restless: A whisper about every 10 minutes.
  • Possessed: A whisper every couple of minutes.

It can also read its whispers aloud using macOS's built-in voice.

There are a few practical considerations, too. Poltergeist goes quiet after five minutes of inactivity, can be paused for an hour when you're sharing your screen, and has an Exorcise option to quit the app.

It's a small project, and it isn't trying to solve a serious productivity problem. I wanted to experiment with what happens when a local language model becomes a little character that reacts to your everyday computer use.

Demo

Code

GitHub logo Aravind-Sathesh / Poltergeist

A ghost in your Mac's menu bar that watches what you do and whispers about it, powered by local Gemma

Poltergeist

A ghost lives in your Mac's menu bar. It watches what you do and every so often it whispers about it.

Seen: Xcode has been the frontmost app for 3 hours 12 minutes Whispered: Three hours in Xcode. The previous owner stayed all night until their fingers bled.

A whisper appearing under the menu bar

The Poltergeist panel showing recent whispers

The whispers are written by Gemma running locally through Ollama. Your shell history, your apps and your habits never leave your Mac.

What it notices

Sense Omen
Shell every command you run in zsh, extra attention for git push --force, rm -rf, sudo, git reset --hard, kill -9 and friends, and the same command run three times in a row
Apps an hour (then two, then three…) in the same frontmost app
Absence coming back after 20+ minutes away, and the Mac waking from sleep
Battery dropping to 20%, 10% and 5% while unplugged
Clock still
…

Poltergeist is built with Swift 6 and SwiftUI using Apple's native frameworks. It has no third-party package dependencies. Gemma runs locally through Ollama.

How I Built It

The interesting part wasn't just generating spooky text. It was getting the model to react to actual activity on the Mac without constantly repeating itself or exposing sensitive information.

Giving the ghost its senses

Poltergeist collects small events called omens. Each omen describes something that happened, such as a command being executed, an app remaining in the foreground for several hours, or the battery dropping below 10%.

The app assigns different weights to these events. Ordinary activity gets a low priority, repeated behaviour gets more attention, and potentially destructive commands or a critically low battery get a higher priority.

Every 15 seconds, Poltergeist checks whether it should speak. When it is time, it selects the most relevant recent omen and sends a short description to Gemma. More serious omens can bypass the usual cooldown.

For shell activity, it reads only new entries appended to ~/.zsh_history after the app starts. It handles zsh's extended history format and multiline commands, and waits for incomplete lines rather than processing partial entries.

Keeping shell history private

This was one of the parts I paid particular attention to.

Shell history can contain API keys, passwords, server addresses, file paths, and other information that shouldn't appear in a notification. Before a command becomes an omen, Poltergeist attempts to redact strings that resemble credentials, tokens, bearer headers, and other secrets.

The model request goes to the local Ollama endpoint at 127.0.0.1:11434. The app doesn't need to send your activity to a remote AI service.

Redaction isn't a guarantee that every possible secret will be caught, so I treat it as an additional safeguard rather than a substitute for local processing.

Finding the right Gemma model

I tested two Gemma configurations through Ollama.

The smaller gemma4:e2b responded in roughly half a second, but its whispers were often too generic. Spending three hours in Xcode might produce something that could apply to almost anyone staring at a screen.

With gemma4:12b, responses took around two to three seconds, but the model was much better at referring to the actual command, application, or battery level that triggered the whisper.

For this project, specificity mattered more than speed. Since the app runs in the background and doesn't need to respond instantly, I chose the 12B model.

Getting the personality right

The first versions had a different problem: the ghost kept repeating variations of the same line.

I made two changes that helped.

First, I added examples to the prompt. These covered situations such as deleting a build directory, spending hours in a browser, working at 3 AM, and running low on battery. This encouraged the model to refer to the actual event instead of producing generic horror dialogue.

Second, each whisper receives a randomly selected mood. There are 12 styles, including Victorian, jealous, reflective about the Mac's previous owner, counting, and nursery-rhyme-like.

I also instructed the model to avoid overused phrases and repetitive openings.

These changes made the output more varied in my tests. The result isn't perfect, but the ghost feels more like a character with an odd personality than a prompt returning the same spooky sentence over and over.

Making it feel like a ghost

My first version used a standard menu and macOS notification banners. It worked, but it looked like an ordinary utility with a Halloween theme.

I replaced that with a custom dark panel and floating, borderless SwiftUI windows for the whispers. The text appears character by character, then fades away. The ghost icon flickers while speaking.

Because the whispers use custom windows rather than notification banners, the app doesn't need notification permission.

The ghost icon itself is drawn in code as a template image, which also provides the basis for the application icon.

Why Does Open Innovation Matter?

For Poltergeist, running an open-weight model locally is important for three reasons.

Privacy matters when the app watches your activity.

Shell commands and application usage can reveal a surprising amount about someone's work. I wouldn't want an app like this sending every command to a cloud service just to generate a joke.

Gemma runs through Ollama on the same Mac, so the model doesn't need access to a remote inference API. This makes local processing part of the design rather than an optional privacy setting.

The app can keep running without per-request costs.

Poltergeist is designed to run quietly in the background, potentially generating whispers throughout the day. Charging for every model call would make that unnecessarily complicated.

Local inference avoids per-whisper API fees, although it still consumes the Mac's memory and processing resources.

I could experiment with the model directly.

I could compare the smaller and larger Gemma configurations, adjust prompts, introduce different moods, and test the results against actual events on my machine.

That freedom was useful because the project's main challenge wasn't simply producing grammatical text. It was making the output specific, varied, and entertaining enough to justify having a ghost in the menu bar.

There are trade-offs. The smaller model was faster but less interesting, while gemma4:12b needs a substantial download and takes a few seconds to respond. Local inference also depends on the hardware available.

For this project, those trade-offs were acceptable. The ghost doesn't need to be instant; it needs to say something worth interrupting you for.

My Agent Session

I used a multi-agent workflow to build Poltergeist. Claude acted as the planner, helping break the project into manageable tasks, work through the architecture, and identify edge cases. Antigravity acted as the implementer, turning those plans into code, running tests, and fixing issues.

I still had to evaluate the model's output and decide what belonged in the application. Getting the ghost to produce spooky text was only part of the work. Making its reactions specific, reducing repetition, and handling shell history carefully were just as important.

Prize Categories

Best Use of Gemma: Poltergeist uses Gemma 4 12B through Ollama to turn real activity on a Mac into context-aware, character-driven whispers. It processes shell commands, app usage, battery levels, and other events locally, then uses the selected event to generate a short response. Prompt examples and 12 randomly selected moods help keep the ghost's personality varied. Running Gemma locally also means the app doesn't need to send a user's activity to a cloud AI service.

Top comments (0)