← Back to list

I Vibe Coded a RAG Chatbot with Claude Code This Weekend. Here’s the Honest Account.

What happens when you hand an AI agent a weekend, a Google Drive full of unread PDFs, and a Star Trek obsession?.

Joao Silva · 2026-05-09 16:36 · 0 claps · 14.2 min read
#vibe-coding #claude-code #rags #generative-ai-use-cases
Open on Medium ↗
Wiki topics: LLM · Large Language Models RAG · RAG & Retrieval AGT · AI Agents AI · AI · General 💻 · Programming

I Vibe Coded a RAG Chatbot with Claude Code This Weekend. Here’s the Honest Account.

What happens when you hand an AI agent a weekend, a Google Drive full of unread PDFs, and a Star Trek obsession?.

I have over 200 technical PDFs and ebooks sitting in Google Drive. Security frameworks, cloud architecture guides, Kubernetes deep-dives, container runtime internals, you name it. I collect them the way people collect gym memberships: with great optimism and almost zero follow-through. The idea is always “I’ll read that next week.” Next week has been arriving for approximately 4 years.

So I built a chatbot that reads them for me. I know, Gemini can do this, but I wanted something mine and more precise.

Over the course of one weekend, using Claude Code (Anthropic’s CLI coding agent) as the primary builder, I put together MyTechBooksWizzard: a RAG (Retrieval-Augmented Generation) system that indexes my Google Drive document library, stores the index in a local vector database, and lets me query it through a chat interface. The frontend is styled after Star Trek’s LCARS computer system, because if you are going to build a personal AI, you might as well make it look like it belongs on the Enterprise.

There is a meta-irony buried in that choice. LCARS stands for Library Computer Access/Retrieval System. I built a retrieval-augmented generation chatbot and dressed it in the skin of a fictional AI whose name is literally “library computer access and retrieval system.” Michael Okuda designed LCARS in 1987. It took us 35 years to actually build one.

This is the honest account of how it went. The impressive bits, the broken bits, and the bits I had to fix myself whilst Claude Code looked on in silence.

The code is open source: https://github.com/ciberjohn/rag-chatbot-lcars

The Stack: What We Built With

Before getting into the war stories, here is the technology stack. I gave Claude Code a description of what I wanted, and it proposed most of this; I steered a few choices.

  • FastAPI (Python 3.12) for the backend API
  • ChromaDB running in a Docker container for vector storage
  • sentence-transformers all-MiniLM-L6-v2 for generating embeddings locally (no external API calls, no per-token cost)
  • Claude Haiku as the language model for actual chat responses (cheapest Claude model available; fine for this use case)
  • rclone to sync Google Drive to a local folder
  • watchdog and APScheduler for detecting file changes and triggering re-indexing automatically
  • Docker Compose for orchestrating the whole thing, bound to 127.0.0.1:8080 only
  • Star Trek LCARS frontend in vanilla HTML, CSS, and JavaScript

The decision to use local embeddings with sentence-transformers was deliberate. Running all-MiniLM-L6-v2 locally means your documents never leave the machine during the indexing phase. The only external traffic is the Claude Haiku API call when you actually ask a question. For a personal document store containing potentially sensitive reference material, that matters.

RAG as an architecture is not magic. It is retrieval plus generation: embed your documents, store those embeddings, embed the user’s query, find the most semantically similar document chunks, then pass those chunks as context to a language model that generates a coherent answer. The language model is not “reading” your documents in real time. It is reading the relevant excerpts that the retrieval step surfaced. Get the retrieval right, and the generation is surprisingly good. Get it wrong, and the model confidently answers a different question.

Claude Code Does the Heavy Lifting

I described the project to Claude Code in a single detailed prompt. I said I wanted a FastAPI backend, ChromaDB for vectors, local sentence-transformers embeddings, Claude Haiku for answers, rclone syncing, automatic re-indexing, and Docker Compose. I mentioned the LCARS frontend as a secondary goal.

What it produced in the first pass was, genuinely, remarkable. A multi-file Python project with proper separation of concerns: an indexer module, a query module, a FastAPI application, Docker Compose configuration with health checks, a .env.example, a proper requirements.txt, and working API endpoints. It included security considerations I had not explicitly asked for: non-root container users, cap_drop: ["ALL"] in the Compose file, SecretStr from Pydantic for the API key, XSS protection via DOMPurify in the frontend, and rate limiting on the API endpoints.

That is someone who has read the OWASP Top 10 and the Docker security best practices and actually retained them.

The first docker compose up did not work. That is not a criticism of Claude Code. Nothing ever works the first time in Docker. What matters is what broke and why.

Bug 1: The ChromaDB Permission Error

Claude Code set user: "1000:1000" on the ChromaDB container. The reasoning is sound: you should not run containers as root. The problem is that the official ChromaDB Docker image, at least in the version pinned at the time, requires root access to write its log file at startup. Container runs, tries to write the log, permission denied, container exits.

The fix is simple: remove the user directive from the chroma service in docker-compose.yml. But the reason it broke is instructive. Claude Code applied a general best practice (non-root containers) to an image that was not designed with that constraint in mind. It had no way to know the internal behaviour of the ChromaDB image without running it. That is a class of bug that only surfaces at runtime, in a specific environment, with a specific image version.

The lesson: security best practices for your own containers. Deference to documentation for third-party images.

Bug 2: The Health Check That Gave Up

The initial design called index_all() synchronously during application startup, before the HTTP server was ready to accept connections. The logic makes sense from a pure correctness standpoint: index everything first, then start serving requests.

The problem: I have 245 documents in that Google Drive folder. Indexing them all (parsing PDFs, generating embeddings, writing to ChromaDB) takes over eight minutes. Docker’s health check has a finite patience. It polls the /health endpoint, gets no response because the server has not started yet, retries a few times, and then marks the container as unhealthy. Depending on the configuration, Docker Compose may refuse to start dependent services or restart the container, creating a boot loop.

The fix is to decouple indexing from startup. The HTTP server needs to start immediately and begin accepting connections. Indexing runs in the background. In FastAPI’s async context, you do this with loop.run_in_executor() without awaiting the result: fire and forget. The server comes up in seconds, the health check passes, and indexing quietly completes in the background. A GET /status endpoint lets you check indexing progress without blocking anything.

@app.on_event("startup")
async def startup_event():
    loop = asyncio.get_event_loop()
    loop.run_in_executor(None, index_all)  # fire and forget

This is a common pattern in production systems and one that newcomers to async Python often miss. The health check exists to serve the orchestrator, not the application’s internal state. Keep them separate.

Bug 3: Volume Permissions and the Named Volume Trap

The application needed to write a file_hashes.json cache to avoid re-indexing documents that had not changed. Claude Code used a Docker named volume for this. Named volumes are managed by Docker and, by default, are owned by root. The application process runs as UID 1000 (the wizard user defined in the Dockerfile). UID 1000 cannot write to a root-owned volume directory.

The fix: replace the named volume with a bind mount pointing to a host directory that you explicitly create and chown to UID 1000.

volumes:
  - /home/ciberjohn/rag-data/hashes:/app/data
mkdir -p /home/ciberjohn/rag-data/hashes
chown -R 1000:1000 /home/ciberjohn/rag-data/hashes

Named volumes are convenient for databases where Docker manages the lifecycle, and you do not care about the host path. For application data that needs to be written by a specific non-root UID, bind mounts with correct host ownership are the right tool. This is one of those Docker nuances that trips up everyone at least once. The symptom (permission denied on write) is clear. The cause (who actually owns the volume mount point inside the container) is less obvious until you have seen it before.

Bug 4: The Ghost in the Machine

After all the Docker work, the application appeared to be running but behaving strangely. The frontend was serving an old version. CPU usage was elevated on the host. Something was wrong.

A quick ps aux | grep uvicorn revealed it: a rogue uvicorn process, still running directly on the host from an earlier manual test session, consuming 164% CPU and serving the old version of the frontend on port 8080. Docker Compose had started its own containers on the same port, but the host process had grabbed the port first, so Docker was actually failing silently, and the host process was answering all requests.

kill -9 <pid>

Done. But the diagnosis is the interesting part. The symptom was “frontend not updating.” The cause was a zombie process from two hours earlier. No AI agent would have caught that one. That required someone who knows how to check for stray processes and who understands port-binding semantics at the OS level. Remember this one?

The rclone Adventure: OAuth on a Headless Server

This one deserves its own section because it is a problem that bites everyone who has ever tried to connect a cloud storage provider to a remote Linux server, and the solution is completely non-obvious unless you already know it.

rclone’s default OAuth flow for Google Drive opens a local browser window, you log in, and it captures the token. Works perfectly on a desktop. Works exactly zero per cent of the time when your server has no display and no browser, which is the case for most servers that actually run services.

The standard advice suggests an SSH tunnel: forward port 53682 from the remote server to your local machine, run rclone config on the server, and complete authentication in your local browser. This works. It is also the kind of multi-step process that feels more complicated than it should be.

The simpler approach, which took me an embarrassing amount of searching to find, is rclone authorize:

On your laptop (with a browser):

rclone authorize "drive"

This opens a browser on your laptop, you authenticate with Google, and rclone prints a token JSON blob to the terminal. You copy that blob, paste it into the rclone config on your server when prompted, and you are done. Then copy ~/.config/rclone/rclone.conf to the server.

scp ~/.config/rclone/rclone.conf user@yourserver:~/.config/rclone/rclone.conf

Simple. Elegant. Completely buried in the documentation. Most tutorials about rclone and Google Drive assume a desktop environment. If you are setting up cloud sync on a headless server, rclone authorize on a machine that has a browser is the path of least resistance.

Bug 5: The Cron Job That Was Not One Line

Once rclone was working, I set up a cron job to sync Google Drive every four hours:

0 */4 * * * rclone sync gdrive:TechBooks /home/ciberjohn/docs/techbooks --transfers=4 --checkers=8 --fast-list 2>>/var/log/rclone-sync.log

This looked fine in the terminal. In the crontab, it was broken. The command, when pasted into crontab -e, had wrapped across two lines in the editor with invisible newlines embedded in the middle of it. cron dutifully reported "bad minute" and ignored the job entirely.

The fix: extract the rclone command to a shell script.

#!/bin/bash
# /home/ciberjohn/scripts/sync-techbooks.sh
rclone sync gdrive:TechBooks /home/ciberjohn/docs/techbooks \
  --transfers=4 \
  --checkers=8 \
  --fast-list \
  2>>/var/log/rclone-sync.log

Then the crontab becomes:

0 */4 * * * /home/ciberjohn/scripts/sync-techbooks.sh

One line. No wrapping. No invisible newlines. This is good practice regardless: cron lines should be short, and complex logic belongs in a script where you can test it independently, add error handling, and read it without squinting.

Bug 6: The CPU Saturation Problem

With everything running, I queried the chatbot with a batch of test questions. It worked. It also made the host machine briefly unusable whilst indexing ran in the background. CPU usage hit 220% sustained (this is a multi-core machine; usage is reported as a percentage of a single core, so 220% means approximately two full cores pinned).

The culprit is sentence-transformers’ default threading behaviour. When you call model.encode(), PyTorch uses all available CPU cores for the operation by default. On a machine with eight cores, this is great for throughput. On a machine where other things need to happen (like serving web requests), it is a problem.

The fix is a single line, placed before you load the model:

import torch
torch.set_num_threads(2)

This caps PyTorch’s intraop parallelism to two threads. Indexing takes slightly longer. The rest of the system remains responsive. For a background task that runs during off-hours sync cycles, this is exactly the right trade-off.

This is documented. But it is also not something that comes up in most RAG tutorials, because most RAG tutorials run on a laptop where CPU saturation during indexing is a five-second inconvenience rather than a service availability problem.

The LCARS Redesign: Because Why Not

Once the backend was stable and queries were returning sensible answers, I turned to the frontend. The initial interface Claude Code had produced was functional and clean: a standard chat window with a text input and a message list. Fine. Boring.

I asked Claude Code to redesign it in Star Trek LCARS style.

What it produced was not a half-hearted application of orange borders. It was a proper LCARS layout: Orbitron font for all UI text, an orange and tan colour palette with blue and purple accents for status indicators, a sidebar featuring a warp core animation built from eight CSS segments that pulse in sequence, a stardate display derived from the current date, a scan bar animation that runs across the screen whilst a query is being processed, and message labels that read COMPUTER and OPERATOR instead of "Assistant" and "You."

All of this in vanilla HTML, CSS, and JavaScript. No frameworks. No dependencies. Just well-structured markup and about four hundred lines of CSS.

LCARS (Library Computer Access/Retrieval System) was designed by Michael Okuda for Star Trek: The Next Generation. The defining aesthetic is rounded elbows connecting horizontal dividers and vertical menus, high contrast colours on a dark background, and a general sense that the interface is both extremely capable and slightly alien. The CSS implementation leans heavily on border-radius, clip-path, and keyframe animations.

Does the LCARS theme make the chatbot more useful? Objectively, no. Does it make me 40% more likely to actually use it? Absolutely yes. Tooling aesthetics matter for personal projects. If the interface brings you joy, you use the tool. If it looks like every other dark-mode Bootstrap template, you open it once and forget it exists.

The Honest Verdict on Vibe Coding

Let me be direct about what Claude Code did well and what it could not do.

What it did well:

Claude Code produced a working, multi-file, multi-service project from a description. It got the architecture right: async FastAPI, proper separation of indexer and query modules, health checks, structured logging, environment variable handling with Pydantic’s SecretStr, rate limiting. It included security defaults I had not asked for. The LCARS redesign was accurate and creative. For the bulk of the work, the things that would take a developer days to scaffold were delivered in hours.

Where it got stuck:

Every single bug I described above sits at an integration boundary. Not within a module, at the seam between systems. The ChromaDB image’s internal UID assumptions. The difference between a named volume and a bind mount for non-root write access. The OAuth flow assumptions baked into rclone’s default configuration. The way a host-level process can shadow a Docker-bound port without any obvious error. The way a long command wraps when you paste it into a terminal editor.

These are not bugs in the code. They are bugs in the assumptions about the environment. And they are exactly the class of problem that does not appear in any training data set, because they manifest only when a specific combination of image version, OS, UID mapping, and configuration choice collide.

A more granular way to put it: vibe coding produces correct code. It produces incorrect assumptions about the world the code runs in.

The 80/20 reality:

Vibe coding gets you 80% of the way there in perhaps 20% of the time it would take to write it yourself. That is a genuine productivity multiplier. It is also not 100%, and the remaining 20% is disproportionately the hard 20%. The integration bugs, the environment-specific failures, and the cases where the documented behaviour and the actual behaviour diverge. Those require someone who understands the underlying systems well enough to form a diagnostic hypothesis and test it.

The term “vibe coding” was coined by Andrej Karpathy in February 2025 and named Collins English Dictionary’s Word of the Year for 2025. It has gone mainstream faster than most technology concepts manage. The UK’s National Cyber Security Centre (NCSC) noted that NCSC CEO Richard Horne addressed vibe coding directly at RSAC 2026, calling it a concept that poses “intolerable risks for many organisations as things stand” whilst acknowledging it represents “glimpses of a new paradigm.” That is not a dismissal. That is a calibration. Use the tools. Know their limits.

The trust figures are instructive here. The 2025 Stack Overflow Developer Survey found that only 29% of developers trust the accuracy of AI coding tools, down from 40% the prior year. That is not cynicism. That is engineering maturity. You learn to use the tools and stay sceptical of the edges simultaneously.

The people who will get the most out of Claude Code and tools like it are not those who are unfamiliar with infrastructure. They are those who know it well enough to recognise when something is wrong and why. You are not replacing expertise. You are multiplying it.

What the Bot Can Actually Do

After all of that, does it work? Yes. Properly.

I can ask it questions like “What does the CIS Benchmark say about SSH hardening?” and it retrieves the relevant chunks from the PDF in my library, cites the source document, and gives me a coherent summary. I can ask “Summarise the key differences between Kubernetes StatefulSets and Deployments from the O’Reilly guide” and it pulls the right content. It handles multi-turn context reasonably well within a session.

It is not perfect. If the PDF is poorly structured (scanned images rather than real text, no OCR), it cannot extract meaningful content. If the query is too vague, the retrieval step surfaces tangentially related chunks, and the answer is plausibly wrong. These are RAG limitations, not ChatBot limitations. The architecture is only as good as the quality of your source documents and the precision of your queries.

For a tool that replaced “I should probably read that PDF someday” with “let me just ask it,” the return on a weekend’s work is extremely good.

Actionable Takeaways

If you are building something similar, here are the things I wish I had known before starting.

1. Use bind mounts when your application runs as non-root in Docker. Named volumes are root-owned by default. If your container process runs as UID 1000, it cannot write to a named volume unless you explicitly fix the permissions. Bind mounts with a host directory you chown to the right UID are more predictable and easier to inspect.

volumes:
  - /host/path/owned/by/1000:/container/path

2. Never block startup with long-running work. Your HTTP server should bind and pass health checks within seconds. Any initialisation that takes more than a few seconds (indexing, model loading, data migration) should run asynchronously in the background. Use loop.run_in_executor() in FastAPI, check progress via a status endpoint.

3. For headless rclone OAuth, authorise on your laptop. Run rclone authorize "drive" on a machine that has a browser, authenticate in the browser, copy the resulting token into your server's rclone config. Then transfer the config file. Do not fight SSH tunnels unless you enjoy it.

4. Cap PyTorch threads in background workers. torch.set_num_threads(2) before loading your model. If your embedding worker is a background task on a shared server, this keeps it from consuming every CPU core during inference and making everything else sluggish.

import torch
torch.set_num_threads(2)
model = SentenceTransformer("all-MiniLM-L6-v2")

5. Write complex cron commands as shell scripts. If your cron command is longer than about sixty characters, put it in a script. Point cron at the script. The command is testable in isolation, readable without squinting at the crontab, and immune to terminal line-wrap corruption.

6. Check for stray processes before concluding your container is broken. ps aux | grep <process_name> is your friend when a Docker container starts, but the application behaves as if it is running an old version. A host process bound to the same port will silently shadow the containerised service.

7. Vibe coding works best when you understand the domain. Use it to accelerate scaffolding, generate boilerplate, explore unfamiliar libraries, and produce first drafts of complex multi-file systems. Be prepared to take over at every boundary between systems. The better you understand infrastructure, the more productively you can use these tools. They amplify expertise. They do not substitute for it.

The Code

Everything described in this article is in the public repository:

**https://github.com/ciberjohn/rag-chatbot-lcars**

It includes the full Docker Compose setup, the FastAPI backend, the indexer and query modules, the rclone sync script, and the LCARS frontend. There are README.md setup instructions. The .env.example documents require every environment variable. PRs are welcome.

The bot is running now. I have already used it more times this week than I have opened any of the PDFs it indexes in the past 12 years. That is probably the most honest measure of whether a personal tool is worth building.

Please make it, use it, and learn something. Engage.


메타데이터
post_id
0806b74db8b8
slug
i-vibe-coded-a-rag-chatbot-with-claude-code-this-weekend-heres-the-honest-account-0806b74db8b8
url
https://medium.com/@joaolealdasilva/i-vibe-coded-a-rag-chatbot-with-claude-code-this-weekend-heres-the-honest-account-0806b74db8b8
canonical_url
https://medium.com/@joaolealdasilva/i-vibe-coded-a-rag-chatbot-with-claude-code-this-weekend-heres-the-honest-account-0806b74db8b8
author_url
https://medium.com/@joaolealdasilva
status
ok
fetched_at
2026-06-09 15:37:30