Using Claude Code with Ollama
A new version of Ollama came out recently and it has the ability to let the Claude Code client connect to it.
I have a project I’ve been trying to get working with Goose and OpenCode, and it’s not been going great.
The TL;DR for the project is I want it to read in all the files in the CVE5 GitHub repo store the data in a sqlite database, then let me view and search it.
I decided to figure this one out because I know this data well and I know about how I want a simple example to work. It’s been literally weeks of trying and I’ve not had any real luck. I’ve gotten things that sort of almost work, but none have had a “just works(tm)” outcome.
When I saw the announcement about the Claude Code client, I thought this was a great time to see if the problems I’m seeing are with Goose and OpenCode, or if it’s a local model problem. (spoiler, I think it’s a local model problem)
The place I’m at now is I had a pretty long description of what I want to project to do. I had GPT OSS 120B turn my description into a plan specifically designed for an LLM. I’ll paste the plan at the end of this post (it’s quite long).
So I ran the plan through Claude Code connected to the Anthropic API. It basically worked. I’ve not dug in super deep, but I got something that reads in all the CVE JSON files, then gives me a nice web UI I can navigate. It took about 5 minutes for Claude Code to spit this out for me.
Qwen3 Coder 30b#
Next I told Claude Code to use my local qwen3-coder:30b model. It did take quite a bit longer to run, 28 minutes, but that’s not a surprise. It’s not running on a super fast GPU.
I would say it did a better job than Goose or OpenCode have done, but I still didn’t have a working system. It wasn’t loading the CVE JSON correctly. I suspect I could fix up my description to get what I want, but I don’t really care enough right now to do that.
Let’s try nemotron 30b next#
Nemotron can be a funny model I’ve found. Sometimes it thinks forever, and sometimes it just spits something out almost immediately. The first time I fed in my plan, it did something for about 3 minutes, then just stopped. So naturally I turned it off and back on again. It spent 17 minutes thinking, then sadly didn’t do anything useful.
What about GPT OSS 120B?#
This also ran for about 30 minutes and didn’t create a working prototype. I would say it’s very comparable to what I see in the Qwen3 Coder build.
What’s next?#
I need to concoct something that’s more simple I think. It seems clear the local models aren’t great at development right now. Hopefully someone creates something better in the future. This could be the sort of thing the Linux Foundation Agentic AI Foundation tries to tackle.
I also have a thought where I take this plan and ask the LLM to break it apart into a ton of small tasks. Then feed those tasks one at a time to a model making minor corrections along the way.
While I’m sure Anthropic won’t be super happy people can use their client against Ollama, I also don’t think they have much to be worried about just yet.
plan.md#
Project Plan – cve‑viewer
Prepared for a coding‑LLM (no code is written here – only a complete, unambiguous specification).
1. Overview#
| Item | Description |
|---|---|
| Name | cve-viewer |
| Purpose | Provide a fast, searchable web UI for the National Vulnerability Database (NVD) CVE JSON files (~300 k files). |
| Tech stack | • Python 3.11+ • Flask (WSGI) on port 8080 • Bootstrap 5 for UI • python-dotenv for env‑var loading • Optional: SQLite + FTS5 or Whoosh for full‑text search |
| Data source | Directory tree of CVE JSON files, located by environment variable CVE_DATA_DIR. The tree is organized by year → sub‑folder of 1 000‑record “shards” (e.g. 2026/0xxx/, 2026/20xxx/, …). |
| Key functional requirements | 1. Home page shows a search box and the latest 25 CVE IDs. 2. Clicking a CVE or submitting a search shows a detail page with the fields listed in the spec. 3. Reference URLs are shown as a bulleted list; other reference data is ignored. 4. No hard‑coded data – the “latest” IDs must be derived from the current‑year directory only. |
| Non‑functional requirements | • Avoid scanning all 300 k files for every request. • Keep memory footprint low – load only what is needed. • Startup time must be reasonable (< 10 s). • Graceful handling of missing / malformed JSON files. |
| Deliverables | • README.md with full description, setup, and run instructions. • requirements.txt. • Flask app (app/). • Jinja2 templates (templates/). • Static assets (static/). • Example .env.example. • Optional Dockerfile (not required but helpful). |
2. High‑Level Architecture#
+-------------------+ +------------------------+ +-------------------+
| Client (Browser) | <----> | Flask (WSGI) | <----> | Index / Cache |
+-------------------+ | - routes: /, /cve/<id> | | (SQLite/JSON) |
| - serves HTML (Bootstrap) |
| - uses service layer |
+------------------------+
-
Service layer (
app/services/) hides the file‑system details. It offers:get_latest_ids(limit=25)– returns the most recent CVE IDs (bypublishedDate).search(term)– returns IDs that match the term (simple substring or full‑text).load_cve(cve_id)– reads the JSON file for a single CVE, validates it, and returns a Python dict ready for templating.
-
Index / Cache – two options (choose one, see § 4).
- Option A – SQLite manifest (preferred). At startup a lightweight SQLite DB is built (or updated) containing the columns we need for list/search (ID, publishedDate, updatedDate, cna, description, reference URLs). The DB file lives next to the app (
data/index.db). - Option B – In‑memory dict – only for prototyping or very small data sets. Entire manifest is loaded into a Python dict keyed by CVE ID; search is a linear scan. Not recommended for production.
- Option A – SQLite manifest (preferred). At startup a lightweight SQLite DB is built (or updated) containing the columns we need for list/search (ID, publishedDate, updatedDate, cna, description, reference URLs). The DB file lives next to the app (
-
File access – individual CVE JSON files are read only when a detail page is requested. The JSON is parsed, the needed fields are extracted, and the result is passed to the template. No other file is touched.
3. Detailed File‑System Layout#
cve-viewer/
│
├─ app/
│ ├─ __init__.py # creates Flask app, loads config
│ ├─ config.py # reads CVE_DATA_DIR, other settings
│ ├─ routes.py # Flask view functions
│ ├─ services/
│ │ ├─ __init__.py
│ │ ├─ manifest.py # SQLite handling / building
│ │ ├─ cve_loader.py # read single JSON file
│ │ └─ search.py # simple or FTS search
│ └─ templates/
│ ├─ base.html
│ ├─ index.html # home page (search + latest list)
│ └─ cve_detail.html # CVE detail view
│
├─ static/
│ └─ (bootstrap CSS/JS if CDN fallback needed)
│
├─ examples/
│ ├─ CVE_Record_Format.json # schema reference
│ └─ CVE-2024-0853.json # sample data
│
├─ data/
│ └─ index.db # SQLite manifest (generated)
│
├─ .env.example # shows CVE_DATA_DIR variable
├─ requirements.txt
├─ README.md
└─ Dockerfile (optional)
- The
examples/directory is never used by the running app – it is only for developer reference. - The
data/folder is created at first run (manifest.build()); it is ignored by version control (.gitignore).
4. Data Indexing Strategy – Options & Recommendation#
| Requirement | Option A – SQLite FTS5 | Option B – In‑Memory dict |
|---|---|---|
| Fast retrieval of latest 25 IDs | ✅ ORDER BY publishedDate DESC LIMIT 25 |
✅ linear scan (slow with 300 k) |
| Full‑text search on description | ✅ FTS5 virtual table (description column) |
❌ would need custom scanning |
| Minimal memory usage | ✅ only DB handles data | ❌ loads all rows into RAM |
| Simplicity of implementation | ✅ sqlite3 is in the Python std‑lib, small wrapper needed |
✅ trivial code |
| Build time on first start | ⏱️ O(N) once (≈ few seconds) | ⏱️ O(N) + RAM cost |
| Updating index when new files appear | ✅ Run incremental “re‑index” script or watch for changes | ❌ must rebuild whole dict |
Recommendation: Implement Option A. The SQLite manifest is created the first time the app starts (or when a --rebuild-index CLI flag is passed). The file is small (< 30 MB) and can be safely shipped with the repo.
If the project later needs to support multi‑process workers (gunicorn), the SQLite file is safe for concurrent reads.
5. Implementation Steps (Task Breakdown)#
Below is a complete, ordered to‑do list for the coding LLM. Each task is atomic and leaves the repo in a buildable state.
- [ ] **Project scaffolding**
- Create top‑level directories: `app/`, `static/`, `templates/`, `data/`, `examples/`.
- Add empty `__init__.py` files where needed.
- Add `.gitignore` (ignore `__pycache__/`, `data/*.db`, `.env`).
- [ ] **Dependency declaration**
- Write `requirements.txt` with: `Flask>=2.3`, `python-dotenv`, `click` (optional), `sqlite3` (builtin), `pytest` (dev only if ever needed), `bootstrap-flask` (optional), `pydantic` (for schema validation – optional but recommended).
- [ ] **Configuration**
- `app/config.py` reads `CVE_DATA_DIR` from `os.getenv`. Raise clear `RuntimeError` if missing.
- Provide `load_dotenv()` call in `__init__.py` so a local `.env` works during dev.
- [ ] **SQLite manifest schema**
- Table `cve`:
```sql
CREATE TABLE cve (
id TEXT PRIMARY KEY,
published TEXT,
modified TEXT,
cna TEXT,
description TEXT,
refs TEXT -- JSON array of URLs (stored as TEXT)
);
```
- Virtual FTS5 table `cve_fts` linked to `cve` for description search:
```sql
CREATE VIRTUAL TABLE cve_fts USING fts5(
description,
content='cve',
content_rowid='rowid'
);
```
- [ ] **Manifest builder (`app/services/manifest.py`)**
- Walk `CVE_DATA_DIR` recursively **only once** (`os.scandir` + `Path.rglob('*.json')`).
- For each file:
* Load JSON (catch `JSONDecodeError` → log & skip).
* Extract needed fields:
- `cve['cveMetadata']['cveId']` → `id`.
- `cve['publishedDate']`, `cve['lastModifiedDate']`.
- `cve['cveMetadata']['assignerShortName']` → `cna`.
- `cve['descriptions'][0]['value']` (or the first description with `lang == "en"`).
- `cve['references']` → map each entry to `.url`; keep only URLs.
* Insert row into `cve` table; insert into FTS table (or let SQLite auto‑populate via `content='cve'` trigger).
- Wrap whole operation in a transaction for speed.
- Provide a CLI entry point (`python -m app.services.manifest rebuild`) for manual re‑indexing.
- [ ] **CVE loader (`app/services/cve_loader.py`)**
- Given a CVE ID, locate the file on disk:
- Derive year = ID.split('-')[1] (e.g., `2024`).
- Determine shard folder by taking the numeric part after the year, zero‑pad, then first digit(s) (e.g., `2024-0853` → `0853` → shard `0xxx` because integer < 1000).
- Build path: `<CVE_DATA_DIR>/<year>/<shard>/<cve_id>.json`.
- Load JSON, extract required fields (same as above), return dict.
- [ ] **Search service (`app/services/search.py`)**
- If using SQLite FTS5: `SELECT id FROM cve_fts WHERE description MATCH ? LIMIT 100;`
- If term is empty → return empty list.
- Provide fallback simple `LIKE` search for environments without FTS5.
- [ ] **Flask app initialization (`app/__init__.py`)**
- Create Flask instance, load config, register blueprint/routes.
- Add error handlers for 404 (CVE not found) and generic 500 (log stack trace).
- [ ] **Routes (`app/routes.py`)**
- `GET /` :
* Read query param `q` (search term). If present → call `search(q)` and render with results.
* If no term → fetch latest 25 IDs via `manifest.get_latest(limit=25)`.
* Render `index.html` (search box + list of links).
- `GET /cve/<cve_id>` :
* Use `cve_loader.load_cve(cve_id)`. If not found → 404.
* Render `cve_detail.html` with fields:
- ID, Published, Updated, CNA (assignerShortName), Description, References (bulleted URLs).
- Optional: `GET /about` static page.
- [ ] **Templates**
- `base.html` loads Bootstrap 5 via CDN, defines a simple navbar with the search form.
- `index.html` extends `base.html`; displays a form (`method="GET"`), and a list (`<ul>`) of the latest IDs or search results (links to `/cve/<id>`).
- `cve_detail.html` extends `base.html`; uses Bootstrap cards to show fields; reference URLs rendered as `<li><a href="{{url}}" target="_blank">{{url}}</a></li>`.
- [ ] **Environment file**
- Create `.env.example`:
```dotenv
# Path to the root folder that contains the year‑wise CVE JSON tree
CVE_DATA_DIR=/path/to/cve-data
```
- Document in README that a real `.env` must be created (or the variable exported).
- [ ] **README.md**
- Project title & badge placeholder.
- Brief description.
- **Prerequisites** – Python 3.11+, pip, optional `make`.
- **Installation** – clone, create virtualenv, `pip install -r requirements.txt`, copy `.env.example` → `.env`, set `CVE_DATA_DIR`.
- **First‑time index build** – `python -m app.services.manifest rebuild`.
- **Running** – `flask --app app run --port 8080` (or via provided `run.py`).
- **Directory layout** – short diagram.
- **Search behavior** – explains that search is case‑insensitive, defaults to description match.
- **Updating data** – after new JSON files are added, re‑run the manifest rebuild command.
- **Docker** (optional) – build and run command snippet.
- **License** – MIT (or whatever).
- [ ] **Dockerfile (optional but helpful)**
- Use `python:3.11-slim`.
- Copy source, install deps, set `ENV CVE_DATA_DIR=/data`, expose `8080`, `CMD ["gunicorn", "-b", "0.0.0.0:8080", "app.routes:app"]`.
- [ ] **Testing the prototype (manual)**
- Populate a tiny `CVE_DATA_DIR` with the two example JSON files (copy them into a fake year folder).
- Run `python -m app.services.manifest rebuild` – verify `data/index.db` exists.
- Start Flask, browse to `http://localhost:8080/` – ensure latest list shows the example CVE.
- Search for a keyword found in the description – verify results.
- Click a CVE link – verify detail page shows required fields with proper formatting.
- [ ] **Documentation of Edge Cases**
- Missing fields: if any required field is absent, replace with “N/A”.
- Malformed reference objects: ignore any entry that does not contain a `url` key.
- Duplicate CVE IDs (should not happen) – first occurrence wins; log warning.
- Large data update: suggest running manifest rebuild as a cron job (e.g., nightly).
- [ ] **Performance considerations**
- Index creation is a one‑time O(N) operation; use `PRAGMA journal_mode=WAL;` for faster concurrent reads.
- Search queries use indexes → sub‑second response even for 300 k rows.
- Detail page loads single JSON file – keep latency < 100 ms by reading only the needed file.
- [ ] **Future extensibility (notes)**
- Add API endpoints (`/api/cve/<id>`) returning JSON for integration.
- Implement pagination for search results.
- Replace SQLite with Elasticsearch for more advanced search (if needed).
- Add background worker to monitor `CVE_DATA_DIR` and incrementally update the index.
6. Risks & Mitigations#
| Risk | Impact | Mitigation |
|---|---|---|
| Index out‑of‑sync (new files appear but not indexed) | Users cannot find newest CVEs. | Provide a clear CLI rebuild-index command; optionally schedule it nightly. |
| File‑system layout deviates (shard naming different) | Loader fails to locate JSON. | Loader falls back to a recursive search within the year directory if the derived path is missing – logs warning. |
| Memory blow‑up (trying to load all JSON at once) | Crash. | Load only one file per request; the index stores only metadata. |
| Invalid JSON in the dataset | Index builder crashes. | Wrap JSON loading in try/except, skip broken files, log filename. |
| Search performance on description without FTS5 | Slow response. | Detect availability of FTS5 (sqlite3.connect(..., detect_types=sqlite3.PARSE_DECLTYPES)), if missing fall back to simple LIKE with a warning. |
7. Final Deliverables Checklist#
- Complete directory scaffold (as in § 3).
-
requirements.txt. - Configuration (
config.py,.env.example). - SQLite manifest builder and CLI.
- CVE loader module.
- Search service (FTS5 & fallback).
- Flask app with routes and error handling.
- Jinja2 templates (Bootstrap 5).
- README with full instructions.
- Optional Dockerfile.
- Documentation of edge‑cases and future enhancements.
When the coding LLM follows the above tasks in order, the resulting cve‑viewer repository will be a functional, maintainable Flask application that fulfills every requirement without any hidden assumptions.