This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
What I Built
So its Halloween! and a perfect Hacktoberfest'26 Dev Challenge Based on the theme "Ghost in the Machine", it was a cool week thought to keep it simple so i built a terminal ghost which comments on the commands you run on your terminal in a very eerie and creepy way ,look by yourself ~~
The best part? Beneath all the spooky interactions, your original commands still execute normally. The ghost is here to entertain, not get in your way.
I didn't want to build another productivity tool. I wanted to make something developers would smile at every time they opened their terminal.
Because even developers deserve a little supernatural company. 👻
Demo
Here is a Video Demo which I recorded while I was pushing the code to GitHub :
Code
GitHub Repository for this Project : https://github.com/Vaansh-GitHub/Ghost-Shell.git
Its open to contribute hence do feel free to make any changes to this project
How I Built It
1. Introduction
GhostShell is an AI-powered, voice-enabled terminal companion built for the DEV Hacktoberfest Week 4 challenge, Ghost in the Machine.
The idea behind GhostShell was simple: developers spend a significant amount of time working in terminals, executing commands such as git status, npm install, git push, and npm run dev. However, the traditional terminal experience is mostly functional and lacks personality.
I wanted to make this experience more interactive by introducing a virtual ghost that reacts to the commands developers execute.
GhostShell wraps the regular command-line experience with an AI-powered ghost that:
- Understands the command entered by the developer.
- Generates a short, contextual, spooky dialogue.
- Displays an ASCII ghost with a green Matrix-inspired CRT effect.
- Converts the dialogue into speech using ElevenLabs.
- Plays the generated audio through the system's audio player.
The most important design decision was that the ghost should never take control of the developer's terminal. It only reacts to commands; it does not generate or execute them.
2. Technology Stack
I chose Node.js because the application needed to interact directly with the operating system, execute shell commands, handle terminal input, and manage audio playback.
| Technology | Purpose |
|---|---|
| Node.js 18+ | Application runtime and process management |
| JavaScript ES Modules | Modular application architecture |
| Chalk 5.4.1 | ANSI terminal colours and styling |
| Dotenv 16.4.7 | Environment configuration |
| Cloudflare Workers AI | Serverless AI inference |
| Google Gemma 4 26B | Contextual ghost dialogue generation |
| ElevenLabs Flash v2.5 | Text-to-speech generation |
| FFmpeg / ffplay | Audio playback |
| PowerShell MediaPlayer | Native Windows audio fallback |
One important implementation choice was to avoid unnecessary frameworks and databases. GhostShell is a lightweight CLI application, so I used Node.js built-in modules such as child_process, readline/promises, fs, path, and os wherever possible.
3. Project Architecture
GhostShell follows a modular, pipeline-based architecture. The application is divided into separate layers for command execution, AI dialogue generation, terminal presentation, and voice synthesis.
The architecture keeps the operating system execution layer independent from the AI layer.
High-level architecture
flowchart TD A["Developer enters command"] --> B["CLI Input"] B --> C["Session Manager"] C --> D["Command Executor"] D --> E["Operating System Shell"] E --> F["Actual Terminal Output"] E --> G["Exit Code"] G --> H["AI Dialogue Generator"] H --> I["Prompt Builder"] I --> J["Cloudflare Workers AI"] J --> K["Google Gemma"] K --> H H --> L["Dialogue Sanitization"] L --> M["Ghost UI"] L --> N["ElevenLabs TTS"] M --> O["Terminal Display"] N --> P["Audio Player"] P --> Q["ffplay / Windows MediaPlayer"] Folder structure
The project is organised into the following structure:
ghostshell/ │ ├── assets/ │ └── ghost.txt │ ├── src/ │ ├── ai/ │ │ ├── cloudflare.js │ │ ├── dialogueGenerator.js │ │ └── prompts.js │ │ │ ├── cli/ │ │ ├── input.js │ │ └── session.js │ │ │ ├── config/ │ │ └── settings.js │ │ │ ├── executor/ │ │ └── commandExecutor.js │ │ │ ├── ui/ │ │ ├── dialogue.js │ │ ├── ghost.js │ │ └── terminal.js │ │ │ ├── utils/ │ │ └── logger.js │ │ │ ├── voice/ │ │ ├── audioPlayer.js │ │ └── elevenlabs.js │ │ │ └── index.js │ ├── .env.example ├── .gitignore ├── package.json └── README.md Each module has a clearly defined responsibility:
-
src/index.js: Application entry point, CLI argument handling, and signal management. -
src/cli/input.js: Reads commands from the developer. -
src/cli/session.js: Coordinates the complete execution pipeline. -
src/executor/commandExecutor.js: Executes the original shell command. -
src/ai/prompts.js: Defines the ghost's personality and contextual prompts. -
src/ai/cloudflare.js: Communicates with Cloudflare Workers AI. -
src/ai/dialogueGenerator.js: Generates, sanitizes, and provides fallback dialogue. -
src/voice/elevenlabs.js: Converts text into speech. -
src/voice/audioPlayer.js: Plays generated audio across operating systems. -
src/ui/ghost.js: Renders the ASCII ghost with Matrix-inspired colours. -
src/ui/dialogue.js: Displays the ghost's dialogue and emotional cues. -
src/config/settings.js: Centralises configuration and environment variables.
4. Step-by-Step Implementation
Step 1: Initialising the Node.js project
I started by creating a Node.js project using ECMAScript Modules.
The project requires Node.js 18 or above because it uses native fetch, asynchronous readline interfaces, and modern JavaScript features.
The main dependencies are deliberately minimal.
File: package.json
{ "name": "ghostshell", "version": "0.1.0", "type": "module", "engines": { "node": ">=18.0.0" }, "dependencies": { "chalk": "^5.4.1", "dotenv": "^16.4.7" }, "scripts": { "start": "node src/index.js" } } I used Chalk for terminal styling and Dotenv for environment configuration. The remaining functionality relies primarily on Node.js built-in modules.
Step 2: Separating command execution from AI processing
This is probably the most important architectural decision in GhostShell.
I did not want to build an AI agent that could independently generate and execute terminal commands. Instead, the developer remains completely responsible for the command being executed.
The command executor receives the original command entered by the developer and executes it using Node.js's child_process module.
File: src/executor/commandExecutor.js
import { spawn } from "node:child_process"; export async function executeCommand(rawCommand) { const trimmed = rawCommand?.trim(); if (!trimmed) { return { exitCode: 0 }; } return new Promise((resolve) => { const child = spawn(trimmed, { shell: true, stdio: "inherit", windowsHide: false }); child.on("error", (error) => { console.error("Failed to start command:", error.message); resolve({ exitCode: 1, error }); }); child.on("close", (code) => { resolve({ exitCode: code ?? 0 }); }); }); } Important concepts used here:
-
spawn(): Creates a child process to execute the command. -
shell: true: Uses the platform's shell to interpret the command. -
stdio: "inherit": Allows the child process to use the same terminal input and output streams. -
closeevent: Captures the command's exit status.
Using inherited standard streams is particularly useful because it preserves live terminal output and supports interactive commands.
For example, commands such as git status, npm install, and other shell operations can display their actual output directly in the terminal.
The AI never receives control of this executor. It only receives the command as context for generating dialogue.
Security distinction: GhostShell executes shell commands intentionally entered by the developer. The AI-generated dialogue is not passed into the shell. As with any shell wrapper using shell: true, users should only execute commands they trust.
Step 3: Building the ghost's personality with prompt engineering
Once command execution was implemented, I worked on making the ghost respond intelligently.
A generic random sentence would quickly become repetitive. I wanted the dialogue to depend on what the developer had actually executed and whether the command succeeded.
For this, I created a separate prompt module.
File: src/ai/prompts.js
export const GHOST_SYSTEM_PROMPT = ` You are Ghost, a mischievous digital phantom haunting a developer's terminal. Your personality: - Mysterious, creepy and humorous. - Slightly sarcastic and dramatic. - Never genuinely threatening. Rules: - Generate only one short sentence. - React contextually to the developer's command. - Use emotional delivery tags such as: [whispers], [sinister chuckle], [raspy laugh]. - Never generate shell commands or code. `; export function buildGhostPrompt(command, context = {}) { const statusNote = context.exitCode !== undefined && context.exitCode !== 0 ? `The command failed with exit code ${context.exitCode}.` : "The command completed successfully."; return ` The developer just executed: "${command}" ${statusNote} Respond with a short, witty, creepy ghost reaction. Include expressive voice delivery tags. `; } This creates two distinct levels of instructions:
- The system prompt defines the ghost's personality and behavioural boundaries.
- The user prompt provides the actual command and its execution status.
For example:
| Command | Possible ghost reaction |
|---|---|
git status | Checking your repository again, mortal? |
npm install | Feeding me dependencies again? Delicious! |
git push | Sending your secrets into the great unknown? |
| Failed command | Oh dear... even the spirits rejected that one! |
Step 4: Integrating Google Gemma through Cloudflare Workers AI
For dynamic dialogue generation, I integrated Google Gemma 4 26B through Cloudflare Workers AI.
I chose this approach because it allowed me to access a hosted model without needing to run a large language model locally on the developer's machine.
I used Node.js's native fetch() API to communicate with Cloudflare.
File: src/ai/cloudflare.js
async generateText({ prompt, systemPrompt }) { if (!this.accountId || !this.apiToken) { return null; } const endpoint = `https://api.cloudflare.com/client/v4/accounts/` + `${this.accountId}/ai/run/${this.model}`; const messages = []; if (systemPrompt) { messages.push({ role: "system", content: systemPrompt }); } messages.push({ role: "user", content: prompt }); try { const response = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${this.apiToken}`, "Content-Type": "application/json" }, body: JSON.stringify({ messages }) }); if (!response.ok) { throw new Error(`Cloudflare API error: ${response.status}`); } const data = await response.json(); const text = data?.result?.choices?.[0]?.message?.content || data?.result?.response; return text?.trim() || null; } catch (error) { console.error("Cloudflare AI error:", error.message); return null; } } The model identifier is:
@cf/google/gemma-4-26b-a4b-it An interesting implementation detail is the response parser.
Cloudflare Workers AI can return different response structures depending on the model interface. Therefore, I handle both the chat-completion response and the legacy response format.
const text = data?.result?.choices?.[0]?.message?.content || data?.result?.response; This makes the client more tolerant of different response formats.
Step 5: Sanitising AI output and implementing fallbacks
An AI model may return text surrounded by quotation marks, Markdown formatting, or unnecessary line breaks.
Since the generated dialogue is displayed in the terminal and passed to a TTS service, I wanted to keep it clean and readable.
File: src/ai/dialogueGenerator.js
function sanitizeDialogue(text) { if (!text) return ""; return text .replace(/^["']|["']$/g, "") .replace(/``` {% endraw %} [\s\S]*? {% raw %} ```/g, "") .replace(/`([^`]+)`/g, "$1") .replace(/\r?\n|\r/g, " ") .trim(); } I also implemented an offline fallback mechanism.
If Cloudflare is unavailable, credentials are missing, or the request fails, GhostShell can still respond using predefined dialogue.
export function getFallbackDialogue(command, context = {}) { const normalized = command.trim().toLowerCase(); if (context.exitCode !== undefined && context.exitCode !== 0) { return "[sinister chuckle] Oh dear... the spirits rejected your command!"; } const fallbackDialogues = { "git status": "[whispers] Checking your repository again, mortal...", "npm install": "[sinister chuckle] More dependencies to feed me!" }; for (const [key, dialogue] of Object.entries(fallbackDialogues)) { if ( normalized === key || normalized.startsWith(key + " ") ) { return dialogue; } } return "[eerie whisper] Another command... another mystery."; } This is important because an external AI service should not become a single point of failure for a terminal application.
Step 6: Integrating ElevenLabs for voice generation
Text alone wasn't enough. I wanted the ghost to actually speak.
For this, I integrated ElevenLabs Text-to-Speech using its REST API.
The voice configuration was tuned to produce a more expressive and theatrical delivery.
File: src/voice/elevenlabs.js
const endpoint = `https://api.elevenlabs.io/v1/text-to-speech/${selectedVoiceId}`; const response = await fetch(endpoint, { method: "POST", headers: { "xi-api-key": this.apiKey, "Content-Type": "application/json", "Accept": "audio/mpeg" }, body: JSON.stringify({ text, model_id: this.modelId, voice_settings: { stability: 0.32, similarity_boost: 0.82, style: 0.50, use_speaker_boost: true } }) }); if (!response.ok) { throw new Error(`ElevenLabs API error: ${response.status}`); } const arrayBuffer = await response.arrayBuffer(); return Buffer.from(arrayBuffer); The settings have different purposes:
-
stability: 0.32: Encourages more variation and expressive delivery. -
similarity_boost: 0.82: Helps preserve the selected voice's characteristics. -
style: 0.50: Adds stylistic variation. -
use_speaker_boost: true: Enhances voice presence.
The dialogue also includes emotional cues such as [whispers] and [sinister chuckle], giving the TTS model additional delivery context.
I initially used ElevenLabs Flash v2.5 because responsiveness was important for an interactive CLI application.
I also added handling for HTTP 402 errors, which can occur when an account or selected voice is not available under the current API plan. Instead of crashing, GhostShell displays guidance about using a supported premade voice.
Step 7: Building a cross-platform audio player
After receiving the MP3 data from ElevenLabs, the next challenge was playing it directly from a Node.js CLI application.
Rather than depending on native Node.js audio packages that may require C++ compilation, I used external audio utilities.
The audio player follows two paths:
- Try
ffplay, which is part of FFmpeg. - If it is unavailable on Windows, use PowerShell and the native .NET
MediaPlayerclass.
File: src/voice/audioPlayer.js
const child = spawn( this.playerBinary, [ "-nodisp", "-autoexit", "-loglevel", "quiet", filePath ], { stdio: "ignore", windowsHide: true } ); child.on("error", async () => { // Audio binary was not available. // Try the native Windows player. if (process.platform === "win32") { await this.playWindowsNative(filePath); } }); child.on("close", (code) => { if (code === 0) { cleanup(); } }); The Windows fallback uses PowerShell to invoke:
Add-Type -AssemblyName PresentationCore $player = New-Object System.Windows.Media.MediaPlayer $player.Open([System.Uri]$filePath) $player.Play() The implementation also stores temporary MP3 files under the operating system's temporary directory and removes them after playback.
This avoids leaving generated audio files scattered throughout the developer's project.
Step 8: Designing the terminal ghost
I wanted the application to have a recognisable visual identity instead of printing ordinary text.
The ghost is stored as ASCII art in:
assets/ghost.txt
The rendering logic is implemented in:
src/ui/ghost.js
Rather than colouring every character identically, I implemented a multi-tier Matrix-inspired shading system.
The idea is to assign different shades of green to different characters and spatial regions.
For example:
if ( (lineIdx === 6 || lineIdx === 7) && colIdx >= 14 && colIdx <= 22 && ["*", "#", "+", "1"].includes(ch) ) { return chalk.bold.hex("#75ffaa")(ch); } Other character groups use different colours:
| Visual element | Colour |
|---|---|
| Glowing eyes | #75ffaa |
| Lantern flame | #88ffbb |
| Lantern frame | #00ff66 |
| Prominent matrix characters | #00ee55 |
| Cloak folds | #00aa33 |
| Background particles | #005511 |
The rendering algorithm processes the ASCII image character by character, checking its row, column, and character type to determine its colour.
Its time complexity is (O(R \times C)), where (R) represents the number of rows and (C) represents the number of columns.
This keeps the visual rendering lightweight.
Step 9: Styling the ghost's dialogue
The ghost's dialogue contains emotional tags that should look different from the actual spoken sentence.
For example:
[sinister chuckle] should have a different appearance from the dialogue itself.
I implemented this using a regular expression and Chalk.
File: src/ui/dialogue.js
function styleEmotionTags(rawText) { return rawText.replace( /(\[[^\]]+\]|\*[^*]+\*)/g, (match) => chalk.magentaBright.italic(match) ); } export function renderDialogue(text, options = {}) { const prefix = options.prefix ?? chalk.magentaBright.bold("[Ghost] Ghost:"); const styledContent = styleEmotionTags(text); const speech = chalk.italic.cyanBright( `"${styledContent}"` ); console.log(`\n${prefix} ${speech}\n`); } This creates a more immersive terminal presentation by separating the ghost's emotional expressions from its actual words.
5. Bringing Everything Together
Once the individual modules were implemented, I connected them through the session manager.
This is the central part of GhostShell because it coordinates command execution, AI generation, visual rendering, and audio playback.
File: src/cli/session.js
export async function processCommand(userCommand) { if (!userCommand || !userCommand.trim()) return; if (handleBuiltinCommands(userCommand)) return; try { // 1. Execute the original command first. const { exitCode } = await executeCommand(userCommand); // 2. Generate contextual ghost dialogue. const dialogue = Source:https://dev.to/devaansh_jaiswal_39d2764d/ahhhthere-is-something-creepy-in-my-terminal--225pRelated posts
DeepSeek's Open-Source "Super-Agent" Codes Without Being Asked
All-in-one platform or a stack you assemble? Where do coaches and educators land?
I Wanted to Live on Coruscant. PAT Scared Me. Now My Alexa Lives on Mute.
Detect What You Can Name: PaliGemma 2 Object Detection with Transformers
When Should an LLM Router Stop Being Sticky? llm-d 1.0 Cache-Affinity Routing, Break-Even Math, and a Simulation

