Ahhh...There is something Creepy in My Terminal ! A developer built GhostShell, an open-source Node.js CLI companion that wraps the terminal and uses Cloudflare Workers AI with Google Gemma to generate eerie, in-character commentary on commands like git status and npm install, optionally voiced through ElevenLabs Flash v2.5 text-to-speech. The tool executes the user's original commands normally and only reacts to them, never generating or running commands itself, with audio played via FFmpeg/ffplay or a Windows MediaPlayer fallback. This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine https://dev.to/challenges/hacktoberfest-week4-2026-10-26 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. 👻 Here is a Video Demo which I recorded while I was pushing the code to GitHub : GitHub Repository for this Project : https://github.com/Vaansh-GitHub/Ghost-Shell.git https://github.com/Vaansh-GitHub/Ghost-Shell.git Its open to contribute hence do feel free to make any changes to this project 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: 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. 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. 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. php 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" 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. 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. 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 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. close event: 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. 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 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: 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 | 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. js const text = data?.result?.choices?. 0 ?.message?.content || data?.result?.response; This makes the client more tolerant of different response formats. 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. js 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. 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 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. 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: ffplay , which is part of FFmpeg. MediaPlayer class. File: src/voice/audioPlayer.js 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: php 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. 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: 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. 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. The ghost's dialogue contains emotional tags that should look different from the actual spoken sentence. 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. 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 = await generateDialogue userCommand, { exitCode } ; // 3. Display the ghost. renderGhost ; // 4. Display its dialogue. renderDialogue dialogue ; // 5. Generate and play the voice. if config.audio.muted { const audioBuffer = await elevenlabsVoice.synthesizeSpeech { text: dialogue } ; if audioBuffer { await audioPlayer.play audioBuffer ; } } } catch error { logger.error "Session processing error:", error.message ; } finally { renderDivider ; } } The complete lifecycle is: The command is executed before AI processing, so a slow AI response does not delay the initial execution of the developer's command. I also wanted GhostShell to behave like a proper terminal environment rather than a program that accepts only one command. For this, I used Node.js's readline/promises module to create an asynchronous input loop. js export async function startSession { renderBanner ; let active = true; while active { const command = await readCommand ; if command { await processCommand command ; } } } The application also supports built-in controls: | Command | Purpose | |---|---| | ghost --help | Displays available options | | ghost --version | Displays application version | | ghost --mute | Toggles ghost voice | | clear / cls | Clears the terminal | | exit / quit | Closes GhostShell | The application supports two main launch modes: Interactive mode: npm start Single-command mode: node src/index.js git status This makes GhostShell useful both as an interactive terminal companion and as a command prefix. Since GhostShell communicates with external APIs, I needed a centralised way to manage credentials and configuration. I used Dotenv and implemented a two-level configuration loader. File: src/config/settings.js js const envPath = path.join projectRoot, ".env" ; if fs.existsSync envPath { dotenv.config { path: envPath } ; } else { dotenv.config ; } const envLocalPath = path.join projectRoot, ".env.local" ; if fs.existsSync envLocalPath { dotenv.config { path: envLocalPath, override: true } ; } The .env.local file takes precedence over .env , allowing developers to maintain private credentials locally. The environment configuration uses variables such as: CLOUDFLARE ACCOUNT ID=your account id CLOUDFLARE API TOKEN=your api token CLOUDFLARE AI MODEL=@cf/google/gemma-4-26b-a4b-it ELEVENLABS API KEY=your api key ELEVENLABS VOICE ID=your voice id ELEVENLABS MODEL ID=eleven flash v2 5 GHOST MUTE=false GHOST AUDIO PLAYER=ffplay LOG LEVEL=info These are placeholders, not actual credentials. I also added .env and .env.local to .gitignore so that local credentials are not accidentally committed to the repository. GhostShell intentionally does not use a database. Since it is a lightweight command-line companion, there is no need to maintain user accounts, sessions, repositories, or persistent application records. This decision provides several advantages: There are only two relevant file-storage mechanisms: The temporary audio files are removed after playback. I wanted GhostShell to remain usable even when external services or audio utilities are unavailable. Therefore, I implemented multiple fallback mechanisms. | Failure scenario | Application behaviour | |---|---| | Cloudflare credentials missing | Uses fallback dialogue | | Cloudflare request fails | Uses predefined ghost responses | | ElevenLabs unavailable | Continues in text-only mode | | Unsupported ElevenLabs voice | Displays actionable configuration guidance | | ffplay unavailable on Windows | Attempts native PowerShell playback | | Audio playback fails | Cleans up temporary files | | Invalid shell command | Captures the process failure and reacts accordingly | This is important because an optional feature such as AI or voice should not make the underlying terminal unusable. The implementation can be divided into eight logical phases. | Phase | Work completed | |---|---| | 1 | Node.js project initialisation and ESM setup | | 2 | Environment configuration and logging | | 3 | Shell command execution engine | | 4 | ASCII ghost and terminal UI | | 5 | Gemma integration through Cloudflare | | 6 | ElevenLabs integration and audio playback | | 7 | Interactive CLI and session orchestration | | 8 | Fallback handling, validation, and documentation | The most important dependency was to get the command executor working independently before integrating the AI. Once the command execution and UI modules were working, the AI and voice integrations could be developed separately and connected through the session manager. There are several improvements that could make GhostShell even better. 1. Background audio playback Currently, the application waits for the ghost to finish speaking before accepting the next command. An optional asynchronous audio mode could make the terminal more responsive. 2. Streaming TTS Instead of downloading the entire MP3 before playback, ElevenLabs streaming could allow the ghost to begin speaking while the rest of the audio is still being generated. 3. Direct audio streaming The MP3 buffer could be piped directly into ffplay through standard input, avoiding temporary disk writes on systems with FFmpeg. 4. Local AI support An optional Ollama or llama.cpp integration could allow users to generate dynamic ghost dialogue locally, without depending on Cloudflare. 5. Additional ghost personalities Different personalities could be introduced, such as a sarcastic ghost, an ancient vampire, or a friendly phantom. These are proposed improvements rather than features claimed to be implemented in the current version. Building GhostShell was an interesting exercise in combining several different areas of software development into one lightweight application. It involved working with operating system processes, asynchronous JavaScript, prompt engineering, hosted LLM inference, text-to-speech APIs, audio playback, terminal rendering, and cross-platform compatibility. The most interesting part was not simply integrating an AI model or a voice API. It was connecting all these technologies while keeping the original terminal experience intact. GhostShell is built around one simple idea: Your terminal executes the commands. The ghost just makes the experience a little more supernatural. Even the command line deserves a little personality. 👻 Open innovation matters because great ideas don't always come from within one company or organisation. Sometimes, the best solutions come when different people share their knowledge, experiences, and skills. It allows companies, startups, universities, researchers, and even ordinary people to work together and turn ideas into something meaningful. For example, in healthcare, a researcher might develop an AI model to detect diseases, a startup could turn it into an application, and a hospital could help test and improve it. By working together, they can create something that would be difficult to achieve alone. Its spirit is based on one of my oldest and the first project "A Timer Application" GitHub Repo : https://github.com/Vaansh-GitHub/Timer-Application https://github.com/Vaansh-GitHub/Timer-Application this was a project which I thought of it back in 2018s and thought of building something on my PC to remind me of time and hence this project. I think that there might be some server error in the GitHub Repository of it which might have caused a malware to crept into it Plss... dont say Ultron is trying to return to the world through my Repo After "Vision Quest" Whatever the case may be now my PC terminal is being watched by someone unknown and creepy and this virus is spreading fast maybe it might also have crept into your pc as you might have opened the Github repo link before reading this section. Now your oldest Repo might also return back into your terminals. Be Ready for the Consequences ? Best Use of Gemma : The Dialogues are written by the AI agent Hosted Over Cloudflare. Best Use of ElevenLabs : The Voice of the ghost is powered by the Voices available at the ElevenLabs.