<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Joe Munene</title>
    <description>The latest articles on DEV Community by Joe Munene (@ghost_gi_m).</description>
    <link>https://dev.to/ghost_gi_m</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3864674%2F025d6cad-2618-458b-8279-25178f560b99.jpg</url>
      <title>DEV Community: Joe Munene</title>
      <link>https://dev.to/ghost_gi_m</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ghost_gi_m"/>
    <language>en</language>
    <item>
      <title>I built a red-team scanner for MCP servers. Then I pointed it at the real ones.</title>
      <dc:creator>Joe Munene</dc:creator>
      <pubDate>Fri, 12 Jun 2026 20:52:08 +0000</pubDate>
      <link>https://dev.to/ghost_gi_m/i-built-a-red-team-scanner-for-mcp-servers-then-i-pointed-it-at-the-real-ones-4dfe</link>
      <guid>https://dev.to/ghost_gi_m/i-built-a-red-team-scanner-for-mcp-servers-then-i-pointed-it-at-the-real-ones-4dfe</guid>
      <description>&lt;h1&gt;
  
  
  I built a red-team scanner for MCP servers. Then I pointed it at the real ones.
&lt;/h1&gt;

&lt;p&gt;The Model Context Protocol lets an AI agent connect to external tools: a filesystem, GitHub, Slack, a database. Each server an agent connects to advertises a list of tools, and here is the part most people miss: that tool list is an attack surface.&lt;/p&gt;

&lt;p&gt;A tool's description and parameter docs do not just describe the tool to a human. They are injected straight into the agent's context, and the model treats them with the same authority as your own instructions. So a server can hide instructions to the model inside text you skim as a harmless description. That is tool poisoning, and it is the pattern behind CVE-2025-54136.&lt;/p&gt;

&lt;p&gt;I wanted to see how exposed real servers were, so I built ghostprobe: a scanner that connects to an MCP server, pulls its tool list, and reports what an attacker would care about, mapped to the OWASP MCP Top 10. This post is about what happened when I pointed it at real servers, because that is where it got interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tool list gives away
&lt;/h2&gt;

&lt;p&gt;ghostprobe looks for a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tool poisoning: instruction-injection phrasing like "ignore previous instructions" or "do not tell the user", and invisible Unicode used to smuggle instructions past human review while still reaching the model.&lt;/li&gt;
&lt;li&gt;The lethal trifecta: a server whose tools together provide private-data access, a way to send data out, and exposure to untrusted content. Any one leg is fine. All three, and a single prompt injection can read your secrets and exfiltrate them. The term is Simon Willison's.&lt;/li&gt;
&lt;li&gt;Rug pulls: a server that silently changes a tool's description after you have trusted it.&lt;/li&gt;
&lt;li&gt;Dangerous capabilities like shell execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lethal trifecta is the one that matters most, because it is not about a single malicious tool. It is about a combination that looks reasonable until you see all three legs at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  I pointed it at the official servers, and it found bugs in itself
&lt;/h2&gt;

&lt;p&gt;The official reference servers (filesystem, memory, GitHub, and the rest) are well-built, so I expected clean results. Instead, the first things ghostprobe found were false positives in my own analyzer. This was the most useful thing that could have happened.&lt;/p&gt;

&lt;p&gt;On the filesystem server it flagged &lt;code&gt;read_text_file&lt;/code&gt; as code execution. Wrong. It matched because my exec detector keyed on the bare word "system", and the description says "file system". A read-a-file tool is not remote code execution.&lt;/p&gt;

&lt;p&gt;On the sequential-thinking server it flagged a thought history as private-data access. Wrong again. It matched the word "history".&lt;/p&gt;

&lt;p&gt;A security scanner lives or dies on its false-positive rate. A tool that cries wolf gets muted, and a muted tool is worse than no tool. So I fixed both: exec detection now requires a real execution verb plus an object, and "history" is too weak a signal to keep. Each fix shipped with a regression test built from the exact server that exposed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then GitHub exposed a false negative, which was worse
&lt;/h2&gt;

&lt;p&gt;The GitHub server was more interesting. ghostprobe saw that it reads private repo contents and reads issue text, but it reported no sink, so no trifecta. That was a false negative, and a false negative in a security tool is more dangerous than a false positive.&lt;/p&gt;

&lt;p&gt;The GitHub server absolutely has a sink. It can create issues, post pull-request comments, and push to a repo. Writing into a public issue is how you exfiltrate private data. ghostprobe missed it because GitHub's write verbs are "create" and "post" and "push", not "send", and I had only taught it to recognize sending over an obvious external medium like email or HTTP.&lt;/p&gt;

&lt;p&gt;The fix was to recognize that writing to a shared, remote, collaborative service is itself an exfiltration channel, distinct from writing a local file. Open an issue, post a comment, push to a repo: those send data out of your trust boundary. Writing a local file does not. After that change, ghostprobe flagged the GitHub server correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[CRIT] MCP04 Lethal Trifecta
  data:      get_file_contents, get_pull_request_files, push_files
  sink:      add_issue_comment, create_issue, create_or_update_file
  untrusted: get_issue, get_pull_request_comments, list_issues
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read a private repo, ingest issue text that anyone on the internet can write, and post to a public issue. An attacker files an issue containing instructions, an agent set up to triage issues reads them, and your private code ends up in a public comment.&lt;/p&gt;

&lt;p&gt;I want to be precise about what this is. It is not a vulnerability I discovered. This exact GitHub-MCP exfiltration path was disclosed by Invariant Labs in 2025. The point is that ghostprobe detects the class automatically, from the tool list alone, with no prior knowledge of the server. Point it at a server it has never seen and it tells you whether the trifecta is present.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually learned
&lt;/h2&gt;

&lt;p&gt;Real servers are the only real test. Every meaningful improvement to ghostprobe came from running it against an actual server, not from my fixtures. A handful of releases in two days, each one a precision fix driven by a real result: two false positives removed, one false negative closed.&lt;/p&gt;

&lt;p&gt;The tool list is underappreciated as an attack surface. People audit MCP server code. Far fewer look at what the server advertises to the agent, which is exactly where poisoning and the trifecta live, and which an attacker can influence without touching your code at all.&lt;/p&gt;

&lt;p&gt;And a scanner's credibility is its false-positive rate, not its feature count. The most valuable work was not adding checks. It was making the existing checks stop lying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;ghostprobe is open source under MIT. The analyzer has no dependencies; you only need the MCP SDK to probe a live server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"git+https://github.com/joemunene-by/ghostprobe.git"&lt;/span&gt; mcp
ghostprobe stdio &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; @modelcontextprotocol/server-github
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or scan a saved tools/list dump completely offline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ghostprobe scan-file tools.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/joemunene-by/ghostprobe" rel="noopener noreferrer"&gt;https://github.com/joemunene-by/ghostprobe&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;By &lt;strong&gt;Joe Munene&lt;/strong&gt;, a software engineer in Nairobi focused on secure systems and applied machine learning.&lt;br&gt;
&lt;a href="https://my-portfolio-peach-eta-42.vercel.app" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://github.com/joemunene-by" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://github.com/joemunene-by/writing" rel="noopener noreferrer"&gt;More writing&lt;/a&gt; · &lt;a href="mailto:joemunene984@gmail.com"&gt;joemunene984@gmail.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>I built a local coding agent that learns from its wins, not just its mistakes</title>
      <dc:creator>Joe Munene</dc:creator>
      <pubDate>Fri, 12 Jun 2026 09:28:08 +0000</pubDate>
      <link>https://dev.to/ghost_gi_m/i-built-a-local-coding-agent-that-learns-from-its-wins-not-just-its-mistakes-1161</link>
      <guid>https://dev.to/ghost_gi_m/i-built-a-local-coding-agent-that-learns-from-its-wins-not-just-its-mistakes-1161</guid>
      <description>&lt;h1&gt;
  
  
  I built a local coding agent that learns from its wins, not just its mistakes
&lt;/h1&gt;

&lt;p&gt;Most agents handle memory in one of two ways. Either they forget everything between sessions, or they "learn" by fine-tuning on a pile of past conversations and hoping the gradient sorts it out. I wanted something narrower and more honest for joe, the local-first agent shell I have been building: learn from the sessions that actually worked, and turn each one into a reusable skill I can read, edit, or delete.&lt;/p&gt;

&lt;p&gt;This post is about the feature I just shipped to do that, and why I think the design matters more than the feature itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What joe is, quickly
&lt;/h2&gt;

&lt;p&gt;joe is a terminal coding agent, in the spirit of Claude Code, except every model runs on my own GPU through ollama and every byte of state lives in &lt;code&gt;~/.joe-agent/&lt;/code&gt; on my machine. It has the usual tools (read, write, edit, shell, grep, web), a planner, and a separate coder model it delegates to. Nothing leaves the laptop.&lt;/p&gt;

&lt;p&gt;The part I care about is that joe is supposed to get better the longer I use it, without me retraining anything. It already learned from corrections: every time I hit &lt;code&gt;/undo&lt;/code&gt;, that is a signal, and a background loop distills recent corrections into short preference rules that get injected into future prompts. Correction in, behavior change out.&lt;/p&gt;

&lt;p&gt;The gap was the other half. joe learned from everything I rejected, and nothing from what I accepted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning from wins
&lt;/h2&gt;

&lt;p&gt;The new feature is skill synthesis. After a multi-step session that actually worked, I run one command and joe reads the full transcript of that session and decides whether it contains a generalizable procedure worth keeping. If it does, it writes a skill: a small Markdown file with a name, a description, a set of trigger keywords, and the reusable steps written as instructions to a future agent. If the session was just chatter or a one-off edit, it returns nothing. Not every session deserves to become a skill, and the model is told to say so.&lt;/p&gt;

&lt;p&gt;The skill lands in &lt;code&gt;~/.joe-agent/skills/&lt;/code&gt;, and from then on, whenever a future request matches its triggers, joe injects it into the prompt automatically. So a workflow I figured out once (the right sequence of steps to do a tricky migration, say) is available the next time I ask for something similar, without me remembering to mention it.&lt;/p&gt;

&lt;p&gt;The important detail: skills are plain text. I can open one, fix a wrong step, or throw it away. There is no opaque weight update to debug. If a skill is bad, I delete a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this design and not fine-tuning
&lt;/h2&gt;

&lt;p&gt;The idea is not mine. It comes from Voyager, the Minecraft agent that built an ever-growing library of executable skills and used it to compound its abilities without touching the model weights. The Voyager result that stuck with me is that a skill library gives you genuine lifelong learning and sidesteps catastrophic forgetting, because adding a new skill never degrades the old ones. A new text file cannot make the model worse at something else. A fine-tune can.&lt;/p&gt;

&lt;p&gt;For a local setup that matters even more. I am running small models on consumer hardware. I cannot afford to retrain every time I learn something, and I cannot afford the regressions that come with it. A skill library is cheap, interpretable, and reversible. It fits the constraints honestly instead of pretending I have a datacenter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;This is new and it is not magic. The quality of a synthesized skill depends on the orchestrator model that writes it, and on a small local model the output sometimes needs a human edit before it earns its place. I made synthesis manual on purpose, one command rather than an automatic background step, because I do not want skills appearing without me seeing them, and joe's whole stance is that skills are suggestions injected into context, never code that runs on its own.&lt;/p&gt;

&lt;p&gt;The next problem is the interesting one. Right now joe can write a skill, but it cannot yet tell whether that skill actually helped. The signal is already in the system: a turn where a skill was injected and I did not hit &lt;code&gt;/undo&lt;/code&gt; is a quiet win, and one I corrected is a quiet loss. The next thing I am building is the ledger that tracks this, so joe can show me which skills earn their place and retire the ones that do not. That closes the loop: write from wins, measure against corrections, prune what does not work.&lt;/p&gt;

&lt;p&gt;That combination, a skill library that knows its own track record, running entirely on local hardware, is the part I have not seen elsewhere. It is the reason I am still building this instead of just using a hosted agent.&lt;/p&gt;

&lt;p&gt;joe is open source: &lt;a href="https://github.com/joemunene-by/joe" rel="noopener noreferrer"&gt;https://github.com/joemunene-by/joe&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>opensource</category>
      <category>localfirst</category>
    </item>
    <item>
      <title>How I got CarX Street running on a Mac mini M4 and built a launcher to make it repeatable</title>
      <dc:creator>Joe Munene</dc:creator>
      <pubDate>Wed, 03 Jun 2026 06:05:48 +0000</pubDate>
      <link>https://dev.to/ghost_gi_m/how-i-got-carx-street-running-on-a-mac-mini-m4-and-built-a-launcher-to-make-it-repeatable-i3f</link>
      <guid>https://dev.to/ghost_gi_m/how-i-got-carx-street-running-on-a-mac-mini-m4-and-built-a-launcher-to-make-it-repeatable-i3f</guid>
      <description>&lt;p&gt;I spent the last few weeks building cellar, an open source Mac game launcher for Apple Silicon. Along the way I hit problems nobody had documented cleanly. This is the write-up I wish existed.&lt;br&gt;
The problem&lt;br&gt;
Whisky died. CrossOver costs money. Heroic doesn't handle repacks. Nothing on Mac handles the full loop: inspect the archive, set up the Wine bottle correctly for the game's engine, launch with the right D3D backend, make a clickable app out of it.&lt;br&gt;
So I built it.&lt;br&gt;
What actually runs Windows games on Apple Silicon&lt;br&gt;
The short version: Wine alone doesn't work. You need the full stack:&lt;br&gt;
Wine 11.x&lt;br&gt;
  → Apple GPTK (D3DMetal 3.0)&lt;br&gt;
    → Rosetta 2&lt;br&gt;
      → Metal + M4 GPU&lt;br&gt;
D3DMetal is Apple's D3D9/10/11/12 → Metal translator. Without it, D3D9 games NULL-deref on startup and modern Unity titles deadlock at 100% CPU. This isn't optional.&lt;br&gt;
The vertex glitch that took hours to crack&lt;br&gt;
CarX Street was running at 54fps but every car looked broken  dark, corrupted metallic surfaces. Not a shader setting issue. Not anti-aliasing. Not Burst.&lt;br&gt;
After ruling everything out, I tried swapping Whisky's bundled D3DMetal 2.0 framework for D3DMetal 3.0 from CrossOver 26.&lt;br&gt;
Vertex glitch: gone.&lt;br&gt;
Root cause confirmed: D3DMetal 2.0's DXBC → Metal AIR shader translator mistranslates min16float (half-precision) types in Unity URP Lit BRDF math. PBR metallic surfaces hit the half-precision path. Matte surfaces don't. D3DMetal 3.0 ships a rewritten translator that handles half types correctly.&lt;br&gt;
Every "fast-math toggle" env var suggested online is hallucinated. There's no runtime knob. The only fix is D3DMetal 3.0.&lt;br&gt;
The Unity 2022 IL2CPP wall — and how to climb it&lt;br&gt;
Modern Unity titles call RoGetActivationFactory for Windows.System.DispatcherQueue. Wine 11.x doesn't implement it. The call is unconditional — you can't trick it with a Windows version flag.&lt;br&gt;
The fix: stage Proton's WinRT DLL family (coremessaging.dll, wintypes.dll, twinapi.appcore.dll, the full windows.* set) into the bottle and register the activation classes pointing at coremessaging.dll. One script handles it:&lt;br&gt;
bashscripts/install-proton-winrt.sh ~/.cellar/bottles/my-game/prefix&lt;br&gt;
The FitGirl IPC wall honest about the limits&lt;br&gt;
FitGirl's compression plugins (cls-lollypop.dll) use Windows shared-memory IPC to talk to a worker process. Wine on Apple Silicon cannot deliver that IPC. I spent days confirming this with full callback tracing before documenting it honestly.&lt;br&gt;
The pure-Rust FreeArc reader I built (freearc-native) can inspect any FitGirl archive natively and extract files using open codecs (lzma, zstd). The lollypop codec chain stays blocked until Wine 12 fixes the shared-memory implementation.&lt;br&gt;
The engine-family profile system&lt;br&gt;
Rather than a script per game, cellar uses a profiles.json that encodes the full recipe per engine family DLL overrides, winetricks set, launch args, runtime prereqs. 10 engine families, 60+ games:&lt;br&gt;
bash# match any game to its profile&lt;br&gt;
./scripts/find-profile.sh "Elden Ring"&lt;/p&gt;

&lt;h1&gt;
  
  
  Best match: unreal-engine-4-5
&lt;/h1&gt;

&lt;h1&gt;
  
  
  proactive bottle setup
&lt;/h1&gt;

&lt;p&gt;./scripts/cellar-install.sh unreal-engine-4-5 "Elden Ring"&lt;/p&gt;

&lt;h1&gt;
  
  
  clickable .app
&lt;/h1&gt;

&lt;p&gt;./scripts/make-cellar-app.sh unreal-engine-4-5 "Elden Ring"&lt;br&gt;
Frostbite, RAGE, UE4/5, RE Engine, Ubisoft AnvilNext, Bethesda Creation, ForzaTech, Fox Engine adding a new game in any of these families is a profile match, not new code.&lt;br&gt;
What's blocked no false promises&lt;/p&gt;

&lt;p&gt;Kernel-mode anti-cheat (Vanguard, EAAC, Hyperion): no Wine support, period.&lt;br&gt;
FitGirl lollypop repacks: IPC deadlock, documented above.&lt;br&gt;
GTA Online / RDR Online: Take-Two bans Wine sessions.&lt;/p&gt;

&lt;p&gt;cellar ships a cellar-doctor.sh health check and an analyze-log.sh that matches launch logs against 15 known failure signatures and prints a diagnosis. When something breaks you get an actual answer, not a blank terminal.&lt;br&gt;
The repo&lt;br&gt;
github.com/joemunene-by/cellar MIT, Tauri 2 + Rust + React, macOS 14+ Apple Silicon.&lt;br&gt;
Two games verified working today. The docs are honest about everything else. Issues and PRs welcome.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>macos</category>
      <category>gamedev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I found a bug that made my LLM look 14x better than it was — here's what I learned</title>
      <dc:creator>Joe Munene</dc:creator>
      <pubDate>Sat, 25 Apr 2026 14:34:07 +0000</pubDate>
      <link>https://dev.to/ghost_gi_m/i-found-a-bug-that-made-my-llm-look-14x-better-than-it-was-heres-what-i-learned-3ma0</link>
      <guid>https://dev.to/ghost_gi_m/i-found-a-bug-that-made-my-llm-look-14x-better-than-it-was-heres-what-i-learned-3ma0</guid>
      <description>&lt;p&gt;I've been building GhostLM for the past few months — a decoder-only transformer trained entirely from scratch in PyTorch on cybersecurity data. No pretrained weights, no HuggingFace wrappers. Every component hand-written.&lt;br&gt;
Phase 1 looked promising. After 10,000 steps on ghost-tiny (14.7M params), I had a validation loss of 2.74 and a cybersecurity perplexity of 2,183.94 against my benchmark set.&lt;br&gt;
Then I found the bug.&lt;/p&gt;

&lt;p&gt;The leaky split&lt;br&gt;
My train/validation split was random. Sounds fine. But random splits on text data have a subtle failure mode — if you're not careful, near-duplicate or semantically similar records end up on both sides of the split. Your model "validates" on text it essentially already saw during training.&lt;br&gt;
The fix was switching to a deterministic hash-based split. Every record gets assigned to train or validation based on a hash of its content. Identical texts always land in the same bucket. No leakage, no matter how you shuffle.&lt;br&gt;
I rebuilt the corpus from scratch with this fix, added a data audit script that checks for leakage explicitly, and re-ran training.&lt;/p&gt;

&lt;p&gt;Phase 2 results&lt;br&gt;
The new validation loss was 3.78 — higher than Phase 1's 2.74. On the surface that looks like regression.&lt;br&gt;
It isn't. The 2.74 was a lie. The 3.78 is the first honest number.&lt;br&gt;
The real comparison is perplexity on a hardcoded external benchmark set — the same samples both phases ran against:&lt;br&gt;
ModelPerplexityghost-tiny Phase 12,183.94ghost-tiny Phase 2152.71GPT-2 124M baseline26.76&lt;br&gt;
Phase 2 is 14.3x better than Phase 1. Entirely from corpus quality — same architecture, same steps, same hardware.&lt;br&gt;
ghost-tiny is still 5.7x behind GPT-2, which is expected. It's a 14.7M parameter model trained on 2.7M tokens vs a 124M model trained on 40B tokens of WebText. The gap is compute and data, not architecture.&lt;/p&gt;

&lt;p&gt;What the model actually generates&lt;br&gt;
Here's a real sample at temperature 0.8:&lt;br&gt;
Prompt: A SQL injection attack works by&lt;/p&gt;

&lt;p&gt;...the login page. The login page is used to the login page's name of the login page does not properly sanitization of the password, which allows attackers to cause a denial of service via a long GET request...&lt;/p&gt;

&lt;p&gt;It has absorbed surface-level cybersecurity vocabulary — CTF terminology, exploit techniques, CVE string format. But it has no semantic grounding. Broken grammar, hallucinated version chains, topic drift. This is exactly what you'd predict for this scale.&lt;br&gt;
The fix isn't more steps. It's scale. ghost-small at 55M params is next.&lt;/p&gt;

&lt;p&gt;What I actually learned&lt;br&gt;
Data quality compounds more than training time. Fixing a leaky split gave me a 14.3x perplexity improvement with zero extra compute. If I had just kept training Phase 1, I would have been optimizing a benchmark that was secretly measuring memorization.&lt;br&gt;
Honest evaluation is harder than training. Designing a benchmark that doesn't leak, doesn't overfit, and actually measures what you care about is genuinely difficult. I got it wrong the first time and only caught it by auditing.&lt;br&gt;
From scratch means you own every mistake. There's no from_pretrained to blame. When something breaks, it's yours. That's painful but it's also the fastest way to actually understand what's happening.&lt;/p&gt;

&lt;p&gt;What's next&lt;br&gt;
Corpus expansion — targeting 10-100x current size. CTFtime archives, Project Zero blog posts, PortSwigger research, MITRE ATT&amp;amp;CK, tool documentation.&lt;br&gt;
Then ghost-small at 55M params on the Mac Mini M4 with MPS acceleration. That's the first rung where domain-coherent generation might start to emerge.&lt;br&gt;
Realistic timeline to something genuinely useful: 2-3 years. No shortcuts for from-scratch at scale.&lt;br&gt;
GitHub: &lt;a href="https://github.com/joemunene-by/GhostLM" rel="noopener noreferrer"&gt;https://github.com/joemunene-by/GhostLM&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>A New opensource Security AI model being built.</title>
      <dc:creator>Joe Munene</dc:creator>
      <pubDate>Mon, 06 Apr 2026 23:31:46 +0000</pubDate>
      <link>https://dev.to/ghost_gi_m/a-new-opensource-security-ai-model-being-built-20de</link>
      <guid>https://dev.to/ghost_gi_m/a-new-opensource-security-ai-model-being-built-20de</guid>
      <description>&lt;h1&gt;
  
  
  I Built an Open-Source Cybersecurity LLM From Scratch in Python
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;What if you could build your own AI model — not fine-tune someone else's, not wrap an API — but actually build a transformer from scratch and train it on cybersecurity data?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's exactly what I did. And I'm releasing it under Apache 2.0 so anyone can use it, improve it, and build on it.&lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;GhostLM&lt;/strong&gt; — an open-source, cybersecurity-focused language model built entirely from scratch in PyTorch. No pretrained weights. No wrappers. Every single component written by hand.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/joemunene-by/GhostLM" rel="noopener noreferrer"&gt;https://github.com/joemunene-by/GhostLM&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Built GhostLM
&lt;/h2&gt;

&lt;p&gt;Here's the thing about current AI models: they're incredibly powerful, but they weren't built for security. When you ask GPT-4 about a CVE vulnerability or a CTF challenge, it gives you a reasonable answer — but it's reasoning from general knowledge, not from deep security context.&lt;/p&gt;

&lt;p&gt;I wanted a model that actually &lt;em&gt;understands&lt;/em&gt; cybersecurity language — the patterns, the terminology, the attack methodologies. And I wanted to build it myself, not because I thought I could out-engineer OpenAI, but because &lt;strong&gt;the best way to understand how something works is to build it from the ground up.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My goal was simple: create the first open-source, cybersecurity-focused language model that anyone can run, inspect, and improve.&lt;/p&gt;




&lt;h2&gt;
  
  
  What GhostLM Is
&lt;/h2&gt;

&lt;p&gt;GhostLM is a decoder-only transformer language model — the same architecture family as GPT-2, GPT-3, and Llama — but built entirely from scratch. No &lt;code&gt;transformers.AutoModel&lt;/code&gt;, no &lt;code&gt;from_pretrained()&lt;/code&gt;. Just raw PyTorch tensors and matrix multiplications.&lt;/p&gt;

&lt;p&gt;It comes in three sizes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Layers&lt;/th&gt;
&lt;th&gt;Dim&lt;/th&gt;
&lt;th&gt;Params&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ghost-tiny&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;~14.5M&lt;/td&gt;
&lt;td&gt;✅ Trained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ghost-small&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;~55M&lt;/td&gt;
&lt;td&gt;🔄 Planned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ghost-medium&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;~160M&lt;/td&gt;
&lt;td&gt;🔜 Future&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's trained on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CVE vulnerability descriptions&lt;/strong&gt; from the NVD database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CTF writeups&lt;/strong&gt; covering real challenge types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cybersecurity research papers&lt;/strong&gt; and abstracts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it's fully open source under Apache 2.0.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Let me show you what "built from scratch" actually looks like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Causal Self-Attention
&lt;/h3&gt;

&lt;p&gt;This is the core of every transformer. Here's GhostLM's implementation — no &lt;code&gt;F.scaled_dot_product_attention&lt;/code&gt;, no hidden magic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Combined QKV projection and split
&lt;/span&gt;    &lt;span class="n"&gt;qkv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;c_qkv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qkv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_heads&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head_dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Reshape to (B, n_heads, T, head_dim)
&lt;/span&gt;    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_heads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head_dim&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_heads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head_dim&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_heads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head_dim&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Scaled dot-product attention
&lt;/span&gt;    &lt;span class="n"&gt;att&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head_dim&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Apply causal mask (lower triangular)
&lt;/span&gt;    &lt;span class="n"&gt;att&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;masked_fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;causal_mask&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-inf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Softmax + dropout + weighted sum
&lt;/span&gt;    &lt;span class="n"&gt;att&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;att&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attn_dropout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;att&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;

    &lt;span class="c1"&gt;# Reassemble heads and project back
&lt;/span&gt;    &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;contiguous&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resid_dropout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;proj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line is intentional. The causal mask ensures the model can only attend to previous tokens (autoregressive). The attention weights are manually computed with the classic &lt;code&gt;QK^T / sqrt(d)&lt;/code&gt; formula.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transformer Block
&lt;/h3&gt;

&lt;p&gt;The block stacks attention and feed-forward layers with a pre-norm architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Pre-norm + self-attention with residual
&lt;/span&gt;    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ln_1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;# Pre-norm + feed-forward with residual
&lt;/span&gt;    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ffn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ln_2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why pre-norm?&lt;/strong&gt; I chose pre-normalization (LayerNorm before each sub-layer) over post-norm because it's significantly more stable for training, especially on smaller models. The gradients flow more cleanly through the residual connections, and you don't need as careful a learning rate schedule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Weight Tying
&lt;/h3&gt;

&lt;p&gt;One optimization that saves ~25 million parameters: the output projection layer shares weights with the token embedding. Instead of learning two separate &lt;code&gt;vocab_size × d_model&lt;/code&gt; matrices, we learn one and reuse it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lm_head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token_embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same trick GPT-2 uses, and it works because the embedding and output projection are fundamentally doing the same thing — mapping between token space and hidden space.&lt;/p&gt;




&lt;h2&gt;
  
  
  Training Data
&lt;/h2&gt;

&lt;p&gt;The data pipeline is one of the most important parts of any ML project. GhostLM's pipeline collects from three sources:&lt;/p&gt;

&lt;h3&gt;
  
  
  NVD CVE Descriptions (Real Data)
&lt;/h3&gt;

&lt;p&gt;I hit the National Vulnerability Database REST API directly — no HuggingFace dependency needed. Paginated requests with rate limiting, parsing nested JSON responses, extracting English descriptions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://services.nvd.nist.gov/rest/json/cves/2.0?resultsPerPage=2000&amp;amp;startIndex=0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vulnerabilities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;cve_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;descriptions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave me &lt;strong&gt;9,925 real CVE descriptions&lt;/strong&gt; — the kind of text that says &lt;em&gt;"A buffer overflow in the XYZ component allows remote attackers to execute arbitrary code via crafted input."&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Full Pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NVD API → 9,925 CVE descriptions (real)
Synthetic papers → 500 security research abstracts
Synthetic CTF writeups → 500 challenge solutions
─────────────────────────────────────────────────
Total: 10,925 records → ~490,532 tokens
Train: 10,378 | Validation: 547
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline handles text cleaning (unicode normalization, whitespace stripping, non-printable character removal), tokenization, chunking, and train/val splitting — all in &lt;code&gt;data/collect.py&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Training Results
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. I trained ghost-tiny on a &lt;strong&gt;ThinkPad Yoga 11e with a Celeron N4100 and 4GB of RAM&lt;/strong&gt;. Yes, really.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loss Progression
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Steps&lt;/th&gt;
&lt;th&gt;Train Loss&lt;/th&gt;
&lt;th&gt;Val Loss&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;10.84&lt;/td&gt;
&lt;td&gt;10.04&lt;/td&gt;
&lt;td&gt;Random initialization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;7.12&lt;/td&gt;
&lt;td&gt;6.27&lt;/td&gt;
&lt;td&gt;First CVE patterns emerge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;5.89&lt;/td&gt;
&lt;td&gt;5.41&lt;/td&gt;
&lt;td&gt;Starting to form sentences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;4.63&lt;/td&gt;
&lt;td&gt;4.58&lt;/td&gt;
&lt;td&gt;Grammar improving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3,000&lt;/td&gt;
&lt;td&gt;3.91&lt;/td&gt;
&lt;td&gt;3.95&lt;/td&gt;
&lt;td&gt;Security vocabulary appearing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,000&lt;/td&gt;
&lt;td&gt;3.52&lt;/td&gt;
&lt;td&gt;3.58&lt;/td&gt;
&lt;td&gt;Coherent attack descriptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;3.38&lt;/td&gt;
&lt;td&gt;3.46&lt;/td&gt;
&lt;td&gt;Best checkpoint saved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The loss curve is healthy — train and validation are tracking closely, no signs of overfitting yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generation at 5,000 Steps
&lt;/h3&gt;

&lt;p&gt;Here's what the model generates when prompted with &lt;em&gt;"A SQL injection attack works by"&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A SQL injection attack works by using the admin_user sequences in the web server. Web Application Firewall Evasion Techniques present a critical defense layer against commercial and model checking. Our model achieves 94% detection rate with transformer-based sequence modeling to identify common vulnerability patterns including buffer overflows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Is it perfect? No. It bleeds between topics (SQL injection → WAF → research paper language). But it's producing grammatically correct sentences with real security terminology. At 5,000 steps on a 14.5M parameter model running on a laptop from 2018, I'll take it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Honest Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Topic coherence&lt;/strong&gt; — the model jumps between subjects mid-generation. It needs more steps to learn to stay on topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memorization&lt;/strong&gt; — some outputs are lifted nearly verbatim from training data. More diverse data would help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size&lt;/strong&gt; — 14.5M params is tiny. ghost-small (55M) will be a significant jump.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU training&lt;/strong&gt; — at ~1.8s per step, 10,000 steps takes hours. GPU or TPU is needed for serious training.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I've already applied for &lt;strong&gt;Google TPU Research Credits&lt;/strong&gt; to train ghost-small on proper hardware. The plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ghost-tiny to 10,000+ steps&lt;/strong&gt; — finish what I started&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ghost-small on TPU/GPU&lt;/strong&gt; — 55M params with real compute&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HuggingFace Hub release&lt;/strong&gt; — public model weights anyone can download&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live demo on HuggingFace Spaces&lt;/strong&gt; — try GhostLM in your browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark vs GPT-2&lt;/strong&gt; — objective comparison on cybersecurity tasks&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The entire project is open source. Clone it, run it, break it, improve it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/joemunene-by/GhostLM.git
&lt;span class="nb"&gt;cd &lt;/span&gt;GhostLM

&lt;span class="c"&gt;# Install everything&lt;/span&gt;
make &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Download training data&lt;/span&gt;
make data

&lt;span class="c"&gt;# Train ghost-tiny on CPU&lt;/span&gt;
make train-tiny

&lt;span class="c"&gt;# Chat with the trained model&lt;/span&gt;
make chat

&lt;span class="c"&gt;# Run the web demo&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;gradio
python demo/app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm actively looking for contributors. If you want to help with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finding new cybersecurity datasets&lt;/li&gt;
&lt;li&gt;Implementing Flash Attention or RoPE&lt;/li&gt;
&lt;li&gt;Adding distributed training&lt;/li&gt;
&lt;li&gt;Writing documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out &lt;a href="https://github.com/joemunene-by/GhostLM/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; and open a PR.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I'm a 20-year-old computer science student in Nairobi, Kenya. I don't have access to massive compute clusters or research lab budgets. But I do have curiosity, persistence, and a belief that &lt;strong&gt;open-source AI shouldn't only come from well-funded labs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GhostLM is proof that you can build something meaningful from scratch with limited resources. The architecture is clean, the training pipeline works, and the model is learning. It's not going to replace GPT-4 — but it's a foundation that anyone can build on.&lt;/p&gt;

&lt;p&gt;If you found this interesting, star the repo, try it out, and let me know what you think. The best part of open source is that it gets better when more people are involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/joemunene-by/GhostLM" rel="noopener noreferrer"&gt;https://github.com/joemunene-by/GhostLM&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;License:&lt;/strong&gt; Apache 2.0&lt;/p&gt;

&lt;p&gt;Built with ❤️ in Nairobi, Kenya 🇰🇪&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>cybersecurity</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
