<?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: lululuhu</title>
    <description>The latest articles on DEV Community by lululuhu (@lululuhu).</description>
    <link>https://dev.to/lululuhu</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%2F4072698%2F6102bb4d-2ed4-4ed0-8717-b034df697a20.png</url>
      <title>DEV Community: lululuhu</title>
      <link>https://dev.to/lululuhu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lululuhu"/>
    <language>en</language>
    <item>
      <title>I lost my best AI prompt after 40 tweaks. So I built a tiny git for prompts.</title>
      <dc:creator>lululuhu</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:16:54 +0000</pubDate>
      <link>https://dev.to/lululuhu/i-lost-my-best-ai-prompt-after-40-tweaks-so-i-built-a-tiny-git-for-prompts-1d5j</link>
      <guid>https://dev.to/lululuhu/i-lost-my-best-ai-prompt-after-40-tweaks-so-i-built-a-tiny-git-for-prompts-1d5j</guid>
      <description>&lt;h1&gt;
  
  
  🤯 We've all been there.
&lt;/h1&gt;

&lt;p&gt;You're iterating on a prompt for an LLM. You tweak one word, the output gets better. You tweak another, it gets worse. You paste it into ChatGPT, it kind of works. You change "concisely" to "in 3 bullets", and suddenly the summaries are perfect.&lt;/p&gt;

&lt;p&gt;Then tomorrow comes. And you have &lt;strong&gt;no idea which version was the good one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your folder looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prompts/
├── summarize.txt
├── summarize_old.txt
├── summarize_v2.txt
├── summarize_v2_final.txt
├── summarize_v2_final_FINAL.txt
├── summarize_REALLY_final.txt
└── summarize_use_this_one.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the worst part? The "good" one might be any of them. 😭&lt;/p&gt;

&lt;p&gt;You can't diff them. You can't tell what changed between &lt;code&gt;v2_final&lt;/code&gt; and &lt;code&gt;v2_final_FINAL&lt;/code&gt;. You can't roll back to the version that actually worked.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 Why not just use git?
&lt;/h2&gt;

&lt;p&gt;Good question. I asked myself the same thing.&lt;/p&gt;

&lt;p&gt;You can. And technically, PromptVault borrows git's entire model — content-addressed objects, trees, commits. But in practice, prompts live next to code. They're mixed into repos, notebooks, chat exports, and random &lt;code&gt;.txt&lt;/code&gt; files on your desktop.&lt;/p&gt;

&lt;p&gt;Prompts deserve their own version control that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📂 Tracks &lt;code&gt;.txt/.md/.prompt/.j2/.yaml&lt;/code&gt; files specifically&lt;/li&gt;
&lt;li&gt;📁 Lives in a &lt;code&gt;.pv/&lt;/code&gt; folder that doesn't collide with your code repo's &lt;code&gt;.git/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🔄 Is built for the &lt;em&gt;iterate-and-compare&lt;/em&gt; workflow that prompt engineering actually is&lt;/li&gt;
&lt;li&gt;🚫 Doesn't require a GitHub repo, doesn't sync to the cloud, doesn't ask for an account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built it. 🛠️&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Enter PromptVault
&lt;/h2&gt;

&lt;p&gt;PromptVault is &lt;code&gt;git&lt;/code&gt;, but purpose-built for prompts. It's written in Rust, ships as a single ~2MB static binary, and runs entirely on your machine. No account, no cloud, no telemetry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pv init
Initialized empty prompt vault &lt;span class="k"&gt;in&lt;/span&gt; ./.pv

&lt;span class="nv"&gt;$ &lt;/span&gt;pv add prompts/summarize.md
added: prompts/summarize.md

&lt;span class="nv"&gt;$ &lt;/span&gt;pv commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"refine: summarize now reports tone + title"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;main 791151d] refine: summarize now reports tone + title
 1 prompt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now iterate. See exactly what changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pv diff prompts/summarize.md
diff &lt;span class="nt"&gt;--&lt;/span&gt; prompts/summarize.md
 You are a precise summarizer.

&lt;span class="nt"&gt;-Summarize&lt;/span&gt; the text below &lt;span class="k"&gt;in &lt;/span&gt;3 concise bullets, &lt;span class="k"&gt;then &lt;/span&gt;propose a title.
+Summarize the text below &lt;span class="k"&gt;in &lt;/span&gt;3 concise bullets, propose a title, and note the tone.

 &lt;span class="o"&gt;{{&lt;/span&gt;text&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walk back through every iteration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pv log
commit 791151d…
parent 4100199…
Date:   Mon Aug 10 10:04:33 2026 +0000

    refine: summarize now reports tone + title

commit 4100199…
Date:   Mon Aug 10 10:04:33 2026 +0000

    feat: initial prompt &lt;span class="nb"&gt;set&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restore any past version by hash, tag, or branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pv show 4100199   &lt;span class="c"&gt;# prefix works too 🔍&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;pv revert v1.0    &lt;span class="c"&gt;# restore working tree to a tagged version ⏪&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🌿 Branches for A/B testing
&lt;/h2&gt;

&lt;p&gt;This is the killer feature for prompt engineering. You want to test two variants of the same prompt? Branch it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pv branch experiment
&lt;span class="nv"&gt;$ &lt;/span&gt;pv checkout experiment
&lt;span class="c"&gt;# ... tweak the prompt, commit it ...&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;pv checkout main       &lt;span class="c"&gt;# working tree restores to main's version&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;pv checkout experiment &lt;span class="c"&gt;# ...and back to experiment's version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch branches and the working tree restores instantly. No copying files, no renaming, no "wait, which folder was the experiment in?" 🎯&lt;/p&gt;

&lt;p&gt;When you're done, compare them against a dataset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pv ab main:summarize.md experiment:summarize.md &lt;span class="nt"&gt;-d&lt;/span&gt; cases.jsonl &lt;span class="nt"&gt;--show&lt;/span&gt;
A/B:  &lt;span class="nv"&gt;A&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;main:summarize.md  &lt;span class="nv"&gt;B&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;experiment:summarize.md  &lt;span class="o"&gt;(&lt;/span&gt;3 cases&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;1/3] DIFF  differs
&lt;span class="nt"&gt;---&lt;/span&gt; A vs B &lt;span class="nt"&gt;---&lt;/span&gt;
&lt;span class="nt"&gt;-A&lt;/span&gt; You are a precise summarizer.
+B You are a concise summarizer.

  hello world
&lt;span class="nt"&gt;---&lt;/span&gt; end &lt;span class="nt"&gt;---&lt;/span&gt;

Summary: 0 identical, 3 differing &lt;span class="o"&gt;(&lt;/span&gt;of 3&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pure local. No model calls. No API keys. 🔒&lt;/p&gt;




&lt;h2&gt;
  
  
  💸 Evals without the API bill
&lt;/h2&gt;

&lt;p&gt;One thing that annoyed me about existing prompt tooling: &lt;strong&gt;everything wants to call a model.&lt;/strong&gt; Every eval run costs money. Every test sends data to OpenAI.&lt;/p&gt;

&lt;p&gt;PromptVault takes a different stance: &lt;strong&gt;it never calls a model.&lt;/strong&gt; It only renders templates and checks assertions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pv &lt;span class="nb"&gt;eval &lt;/span&gt;summarize.md &lt;span class="nt"&gt;--dataset&lt;/span&gt; cases.jsonl &lt;span class="nt"&gt;--show&lt;/span&gt;
Eval: summarize.md  &lt;span class="o"&gt;(&lt;/span&gt;3 cases&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;1/3] PASS  contains &lt;span class="s2"&gt;"3 concise bullets"&lt;/span&gt;
&lt;span class="nt"&gt;---&lt;/span&gt; rendered prompt &lt;span class="nt"&gt;---&lt;/span&gt;
You are a precise summarizer.
...
&lt;span class="nt"&gt;---&lt;/span&gt; end &lt;span class="nt"&gt;---&lt;/span&gt;

Summary: 2/2 passed &lt;span class="o"&gt;(&lt;/span&gt;100%&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You write a JSON Lines dataset, each line fills the prompt's &lt;code&gt;{{variables}}&lt;/code&gt;, and PromptVault renders + asserts. ✅&lt;/p&gt;

&lt;p&gt;If you want to actually run the prompt through a model, pipe the rendered output to whatever runner you trust (curl, the OpenAI CLI, ollama, your own script).&lt;/p&gt;

&lt;p&gt;This separates two concerns that existing tools conflate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Did my template render correctly?&lt;/strong&gt; (cheap, local, deterministic) ⚡&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the model produce good output?&lt;/strong&gt; (expensive, non-deterministic, model-dependent) 💰&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;PromptVault does (1). You choose how to do (2).&lt;/p&gt;




&lt;h2&gt;
  
  
  📋 The full feature list
&lt;/h2&gt;

&lt;p&gt;For a "tiny git for prompts", it ended up with more than I planned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📸 &lt;strong&gt;Snapshots&lt;/strong&gt; with messages (like git commits)&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Line-level diff&lt;/strong&gt; so you can see &lt;em&gt;exactly&lt;/em&gt; which instruction you changed. &lt;code&gt;pv diff --stat&lt;/code&gt; for a one-line summary per file.&lt;/li&gt;
&lt;li&gt;🌿 &lt;strong&gt;Branches &amp;amp; merging&lt;/strong&gt; — &lt;code&gt;pv branch experiment&lt;/code&gt;, &lt;code&gt;pv merge experiment&lt;/code&gt; (fast-forward or three-way, with conflict markers)&lt;/li&gt;
&lt;li&gt;🏷️ &lt;strong&gt;Tags &amp;amp; rollback&lt;/strong&gt; — &lt;code&gt;pv tag v1.0&lt;/code&gt;, &lt;code&gt;pv revert v1.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🚫 &lt;strong&gt;&lt;code&gt;.pvignore&lt;/code&gt;&lt;/strong&gt; — gitignore-style, so drafts stay out of the vault&lt;/li&gt;
&lt;li&gt;🔀 &lt;strong&gt;Ref-to-ref diff&lt;/strong&gt; — &lt;code&gt;pv diff v1 v2&lt;/code&gt; compares any two commits/tags/branches&lt;/li&gt;
&lt;li&gt;📂 &lt;strong&gt;Ref:path access&lt;/strong&gt; — &lt;code&gt;pv show HEAD:summarize.md&lt;/code&gt; reads any file at any ref&lt;/li&gt;
&lt;li&gt;🧰 &lt;strong&gt;Stash / reset / clean / grep / export / stats / blame&lt;/strong&gt; — everyday git-class utilities&lt;/li&gt;
&lt;li&gt;🖥️ &lt;strong&gt;TUI&lt;/strong&gt; — &lt;code&gt;pv tui&lt;/code&gt; launches an interactive commit browser&lt;/li&gt;
&lt;li&gt;🐚 &lt;strong&gt;Shell completions&lt;/strong&gt; — bash / zsh / fish / elvish / powershell&lt;/li&gt;
&lt;li&gt;☁️ &lt;strong&gt;Remote sync&lt;/strong&gt; — &lt;code&gt;pv push&lt;/code&gt; / &lt;code&gt;pv pull&lt;/code&gt; syncs the vault to any git host as a backing store&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;Model runner&lt;/strong&gt; &lt;em&gt;(opt-in, build with &lt;code&gt;--features run&lt;/code&gt;)&lt;/em&gt; — &lt;code&gt;pv run&lt;/code&gt; against OpenAI/Anthropic/Ollama. &lt;strong&gt;API keys live only in env vars; nothing is ever logged or stored.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;⚖️ &lt;strong&gt;A/B testing&lt;/strong&gt; — &lt;code&gt;pv ab main:x experiment:x -d dataset.jsonl&lt;/code&gt; renders two versions against the same dataset and diffs them&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔧 How it works under the hood
&lt;/h2&gt;

&lt;p&gt;PromptVault is a tiny git. On &lt;code&gt;pv init&lt;/code&gt; it creates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.pv/
├── HEAD              → "ref: refs/heads/main"
├── index.json        → staging area (path → blob hash)
├── objects/          → content-addressed store (SHA-256)
│   └── ab/cdef…      → "&amp;lt;type&amp;gt;\0&amp;lt;data&amp;gt;"  (blob / tree / commit)
└── refs/heads/main   → latest commit hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every prompt version is a &lt;strong&gt;blob&lt;/strong&gt; addressed by the SHA-256 of &lt;code&gt;blob\0&amp;lt;content&amp;gt;&lt;/code&gt;. A &lt;strong&gt;tree&lt;/strong&gt; maps paths to blobs. A &lt;strong&gt;commit&lt;/strong&gt; points to a tree + parent + message. Identical content is stored once. Nothing ever leaves your machine unless you explicitly &lt;code&gt;pv push&lt;/code&gt;. 📦&lt;/p&gt;

&lt;p&gt;It uses &lt;strong&gt;Myers diff&lt;/strong&gt; for line-level changes (same algorithm as git), and the glob matcher for &lt;code&gt;.pvignore&lt;/code&gt; is a dynamic-programming implementation to avoid the exponential backtracking that naive recursive matchers hit on patterns like &lt;code&gt;*a*a*a*&lt;/code&gt;. 🧠&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Install
&lt;/h2&gt;

&lt;p&gt;Three options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Prebuilt binary (no Rust toolchain needed) 📥&lt;/span&gt;
&lt;span class="c"&gt;#    Download from https://github.com/lululuhu/PromptVault/releases&lt;/span&gt;
&lt;span class="c"&gt;#    Available for: Linux/macOS/Windows, x86_64 and aarch64&lt;/span&gt;

&lt;span class="c"&gt;# 2. cargo 🦀&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;promptvault

&lt;span class="c"&gt;# 3. From source 🔧&lt;/span&gt;
git clone https://github.com/lululuhu/PromptVault
&lt;span class="nb"&gt;cd &lt;/span&gt;PromptVault
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;span class="c"&gt;# binary: target/release/pv  (put it on your PATH)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in any folder where you keep prompts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pv init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You're versioning prompts. 🎉&lt;/p&gt;




&lt;h2&gt;
  
  
  🔒 The trust model
&lt;/h2&gt;

&lt;p&gt;A quick word on this, because I think it matters.&lt;/p&gt;

&lt;p&gt;PromptVault is &lt;strong&gt;local-first&lt;/strong&gt;. Everything lives in &lt;code&gt;.pv/&lt;/code&gt; on your machine. No account, no cloud, no telemetry, no analytics, no "phone home". The only network calls are the ones &lt;em&gt;you&lt;/em&gt; explicitly make:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;☁️ &lt;code&gt;pv push&lt;/code&gt; / &lt;code&gt;pv pull&lt;/code&gt; to sync to a git remote you configured&lt;/li&gt;
&lt;li&gt;🤖 &lt;code&gt;pv run&lt;/code&gt; (opt-in feature) to send a rendered prompt to a model API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;pv run&lt;/code&gt; command reads API keys &lt;strong&gt;only&lt;/strong&gt; from environment variables. Nothing is logged. Nothing is stored beyond the rendered prompt itself. 🔐&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important caveat:&lt;/strong&gt; if your prompts contain secrets, PII, or confidential information, committing them to a vault stores that data on disk in plaintext (content-addressed, but unencrypted). Pushing to a remote sends it to that git host. Same rule as git — don't commit secrets you wouldn't commit to git. ⚠️&lt;/p&gt;




&lt;h2&gt;
  
  
  🗺️ What's next
&lt;/h2&gt;

&lt;p&gt;This is v0.2.0. The roadmap ahead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🤝 Better conflict resolution UX (currently three-way merge produces conflict markers; a &lt;code&gt;pv mergetool&lt;/code&gt; is planned)&lt;/li&gt;
&lt;li&gt;📜 Prompt templates with conditionals/loops (right now it's just &lt;code&gt;{{var}}&lt;/code&gt; substitution)&lt;/li&gt;
&lt;li&gt;🌐 A web UI for browsing history (the TUI is nice but a browser is nicer for sharing)&lt;/li&gt;
&lt;li&gt;🪝 Hooks (pre-commit, post-checkout) for automation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have opinions, the issue tracker is open. 🚪&lt;/p&gt;




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

&lt;p&gt;If you've ever lost a good prompt to "v2_final_FINAL.txt" syndrome, give it a spin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🐙 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/lululuhu/PromptVault" rel="noopener noreferrer"&gt;https://github.com/lululuhu/PromptVault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;crates.io:&lt;/strong&gt; &lt;code&gt;cargo install promptvault&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Releases (prebuilt binaries):&lt;/strong&gt; &lt;a href="https://github.com/lululuhu/PromptVault/releases" rel="noopener noreferrer"&gt;https://github.com/lululuhu/PromptVault/releases&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⭐ Star it if it's useful.&lt;br&gt;
🐛 Open issues if it's not.&lt;br&gt;
I'm building this in the open and good ideas ship fast.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;PromptVault is MIT-licensed, written in Rust 🦀, and runs on Linux/macOS/Windows. The author has no affiliation with any AI lab.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
