I Built a Private AI Assistant That Queries My Git History and Project Management Data — Using Only Local LLMs

No API keys. No cloud. All data stays on my machine.

The Problem

As a web developer, I constantly need to answer questions like:

  • “Who committed the most to our main repo this month?”
  • “What files were changed for the last campaign launch?”
  • “What project tasks are still in progress for the web team?”

These answers exist — scattered across git log, project management boards, and my own memory. I was tired of digging through terminal output and clicking through boards manually.

So I built a natural language interface that lets me ask these questions in plain English and get instant answers.

The Architecture: Text-to-SQL, Not Vector RAG

Here’s the key insight that shaped the entire project:

My data is structured, not unstructured. Commits have authors, dates, and repos. Project tasks have statuses, deadlines, and assignees. This isn’t a pile of PDFs — it’s relational data that fits naturally into a SQLite database.

Traditional RAG (vector embeddings + similarity search) is built for unstructured documents. For structured data, there’s a better approach: Text-to-SQL.

User Question
    ↓
Local LLM (generates SQL)
    ↓
SQLite Database (executes query)
    ↓
Local LLM (summarizes results)
    ↓
Human-readable Answer

The LLM doesn’t store or memorize my data. It just translates my question into SQL, runs it, and explains the results.

The Data Pipeline

Step 1: Collect everything into SQLite

I wrote two Python collectors that populate a single SQLite database:

Git history collector (collect.py):

  • Runs git log across multiple repositories
  • Stores commits, file changes, branches, and tags
  • Captures author, date, message, and insertions/deletions per file

Project management collector (collect_pm.py):

  • Queries the project management platform’s GraphQL API (Monday.com in my case, but the pattern works for Jira, Linear, etc.)
  • Stores boards, items, and subitems
  • Extracts status, assignee, department, and deadline
  • Flags web-team tasks automatically (is_web = 1)

The result: a single SQLite database holding everything needed to answer cross-cutting questions.

Step 2: Link git branches to project tasks

This was the crucial step. Git branches like feature/example-promo-banner don’t obviously connect to project items like “Example Promo Banner — Launch”.

I created a branch_task_map table that links them:

SELECT branch_name, task_name, board_name
FROM branch_task_map
WHERE branch_name LIKE '%promo-banner%'

This lets the system cross-reference: “What tasks relate to this branch?” or “What commits were made for this launch?”

The RAG System

Why Ollama?

Privacy was non-negotiable. Project data, commit messages, and task details shouldn’t leave the machine. Ollama runs the LLM entirely locally — no internet needed, no data sent anywhere.

I chose qwen2.5-coder:7b as the model — it’s excellent at SQL generation and runs fast on Apple Silicon.

The smart prompt

The system prompt is where the magic happens. It includes:

  1. Full database schema — auto-introspected at startup
  2. Sample values — actual repo names, anonymized author identifiers, statuses from the database
  3. Few-shot SQL examples — teaches the model the query patterns
  4. Today’s date — so “this week” and “last month” work correctly
def build_system_prompt():
    schema = get_schema()         # Auto-introspect SQLite tables
    samples = get_sample_values() # Real values from the DB
    return f"""You are a data analyst assistant...

## Database Schema
{schema}

## Sample Values
{samples}
..."""

Auto-discovery: the secret sauce

Before the LLM even sees the question, the system extracts keywords and searches across all tables:

def discover(question):
    keywords = extract_keywords(question)
    # Search task_boards, task_items, commits, branches...
    # Return matching IDs, names, values

This means when you ask “What’s happening with the example promo banner launch?”, the system has already found:

  • The matching project board
  • Related branches: feature/example-promo-banner
  • Recent commits referencing the same keywords

The LLM gets these exact values, so it writes precise SQL instead of guessing.

Self-correcting queries

If a SQL query returns 0 results, the system automatically retries with different keyword strategies:

Attempt 1: WHERE branch = 'feature/example-promo-banner'  → 0 results
Attempt 2: WHERE message LIKE '%promo banner%'             → 12 results

This handles the reality that commits are often on parent branches, not the feature branch itself.

The Result

A CLI tool where I type questions and get answers:

$ python3 main.py "who committed the most this month?"

Developer A and Developer B lead this month
with roughly 350 commits each, followed by
Developer C with around 280 commits.
$ python3 main.py "what web tasks are pending for the next launch?"

The upcoming launch has 8 web tasks remaining:
3 in progress, 2 ready for review, 3 not started...
$ python3 main.py -v "what files changed for the example promo banner?"

-- SQL: SELECT DISTINCT fc.file_path, SUM(fc.insertions)...
-- WHERE c.message LIKE '%promo banner%'...

Several template and snippet files were modified,
concentrated in the promo banner section and a few
related shared components.

Project Structure

The entire system is 8 files, ~400 lines of code, 2 dependencies:

custom-rag/
  main.py           # CLI entry point — REPL + one-shot mode
  agent.py          # LLM conversation loop (question → SQL → answer)
  db.py             # SQLite read-only, schema introspection, auto-discovery
  prompts.py        # System prompt with schema + few-shot examples
  tools.py          # Tool definitions
  formatter.py      # Rich terminal output
  config.py         # Paths and model settings
  requirements.txt  # rich, requests (that's it)

No LangChain. No vector database. No embeddings. No cloud services.

Key Takeaways

  1. Not all RAG needs vectors. If your data is structured, Text-to-SQL is simpler and more accurate than embedding everything into a vector store.
  2. Local LLMs are production-ready. Ollama + qwen2.5-coder:7b runs fast on a MacBook and generates correct SQL reliably.
  3. Auto-discovery beats prompt engineering. Instead of hoping the LLM guesses the right table values, search the database first and feed it exact matches.
  4. Privacy and simplicity can coexist. The whole system is a few hundred lines of Python, runs offline, and handles real questions.
  5. Cross-referencing is the real value. Any single data source is easy to query manually. The power comes from connecting git history with project management data in one natural language interface.

What’s Next

  • Cron job to auto-refresh data every hour
  • Adding chat message history as a third data source
  • A simple web UI for non-terminal users
  • Open-sourcing a generic version of the tool

Built with Python, SQLite, Ollama, and qwen2.5-coder. All code runs locally — no data leaves the machine. All examples in this article use illustrative names and rounded figures; real commit authors, project codenames, and counts have been replaced or generalized.

If you’re interested in the implementation details or want to build something similar, feel free to reach out or comment below.

Multica: An Open-Source Platform for Managing AI Coding Agents Like Teammates

If you’ve been using Claude Code, Codex, or similar AI coding agents, you’ve probably felt the friction: you paste a prompt, watch the run, babysit the output, copy something into the next prompt, and repeat. It works, but it doesn’t scale — and it definitely doesn’t feel like working with a team.

Multica is an open-source project that tries to fix that. The pitch is simple: treat your AI agents the way you treat human teammates. Assign them issues. Watch them post updates. Let them report blockers. Have them compound skills over time.

What Multica Actually Does

At its core, Multica gives your coding agents a place to live inside your team’s workflow. Instead of operating a chatbot in isolation, you assign tasks to an agent the same way you’d assign a GitHub issue to a colleague. The agent picks it up, executes it on a runtime (your local machine or a cloud instance), streams progress back in real time, and posts comments when it needs clarification or hits a wall.

A few things stand out:

Task lifecycle management. Tasks move through states: enqueue, claim, start, complete, or fail. You’re not just running a command and hoping — you have visibility into where each task is.

Reusable skills. When an agent solves something well — a deployment script, a migration pattern, a code review checklist — that solution becomes a reusable skill the whole team can pull from. Skills accumulate over time, which is where the “compound” part of the tagline comes from.

Multi-agent, multi-workspace. You can have multiple agents running on different runtimes, organized into workspaces. Each workspace is isolated with its own issues, agents, and settings.

Vendor-neutral. Multica works with Claude Code, Codex, OpenCode, OpenClaw, Hermes, Gemini, Pi, and Cursor Agent. You’re not locked into one provider.

The Architecture in Plain Terms

The stack is straightforward:

┌──────────────┐     ┌──────────────┐     ┌──────────────────┐
│   Next.js    │────>│  Go Backend  │────>│   PostgreSQL     │
│   Frontend   │<────│  (Chi + WS)  │<────│   (pgvector)     │
└──────────────┘     └──────┬───────┘     └──────────────────┘
                            │
                     ┌──────┴───────┐
                     │ Agent Daemon │  runs on your machine
                     └──────────────┘
  • Frontend: Next.js 16 with the App Router
  • Backend: Go with the Chi router, sqlc for type-safe queries, and gorilla/websocket for real-time streaming
  • Database: PostgreSQL 17 with pgvector (for skill embeddings and similarity search)
  • Agent runtime: A local daemon that auto-detects whatever agent CLIs you have on your PATH

The daemon is the key piece. It bridges your machine (where the actual agent CLI lives) with the Multica server (cloud or self-hosted). When an agent is assigned a task, the server routes it to the appropriate daemon, which spawns the CLI process and streams output back via WebSocket.

Getting Up and Running

Installation is one line:

macOS / Linux:

brew install multica-ai/tap/multica
# or without Homebrew:
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash

Windows:

irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex

Then connect everything:

multica setup   # authenticate + start the daemon in one command

After that, open the web app, go to Settings → Runtimes, and you should see your machine listed. From there you create an agent (pick a provider and runtime), and you’re ready to assign tasks.

The CLI surface is minimal:

Command What it does
multica setup One-shot: configure, authenticate, start daemon
multica daemon start Start the local runtime manually
multica daemon status Check what’s running
multica issue list List your workspace issues
multica issue create Create a new issue
multica update Pull the latest version

If you want to self-host the whole thing (server included), add --with-server to the install script:

curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --with-server
multica setup self-host

This pulls the official Docker images from GHCR. You’ll need Docker. The full self-hosting guide lives in SELF_HOSTING.md in the repo.

For Contributors

The dev setup is a single command:

make dev

It auto-detects your environment, sets up the .env file, installs dependencies, runs DB migrations, and starts all services. Prerequisites: Node.js v20+, pnpm v10.28+, Go v1.26+, and Docker.

The codebase is about 53% TypeScript and 43% Go — frontend and backend are clearly separated, which makes it easy to work on one without touching the other.

Multica vs Going Solo With an Agent CLI

Here’s an honest comparison of what Multica adds versus just running claude or codex directly:

What you gain:

  • A shared board where your whole team sees what agents are working on
  • Real-time streaming progress instead of waiting for a long CLI run to finish
  • A skills library that accumulates team knowledge rather than living in individual prompts
  • Multi-agent routing — different tasks can go to different agents on different machines
  • An audit trail: who assigned what, when, and what happened

What you take on:

  • Running a daemon process (or a full server if self-hosting)
  • A PostgreSQL database
  • The overhead of a web app and task board

If you’re a solo developer running occasional agent tasks, the CLI alone might be enough. If you’re on a team — even a small one — trying to coordinate multiple agents across projects, Multica addresses a real gap.

Is It Worth Trying?

It depends on where you are with AI agents.

If you’re still experimenting and running agents manually on single tasks, Multica is probably more infrastructure than you need right now. Start with the agent CLI directly and see what breaks.

If you’ve gotten past that point and are starting to feel the coordination pain — agents running on different machines, teammates not knowing what’s been automated, the same solutions being re-invented in different prompts — that’s exactly the gap Multica is designed to fill.

It’s early software (v0.2.x as of this writing), so expect rough edges. But the core loop — assign, execute, report, reuse — is working, and the momentum behind the project is real.

Links:

The Death of the Junior Developer

Somewhere right now, a hiring manager is looking at a requisition for a junior developer and asking a question that did not exist two years ago: why would I hire someone to do what my senior developers and their AI tools already handle?

Across the industry, a CS graduate is staring at a job board that looks nothing like the one their professors described. Entry-level postings are down 60% since 2022. The career path they were promised has a gap where the first rung used to be.

Both of them are right to be concerned. Neither of them has a good answer.

The Numbers Are Not Subtle

A Harvard study tracking 62 million workers across 285,000 U.S. firms found that companies adopting generative AI cut junior employment by 7 to 12 percent within six quarters. Senior employment stayed flat. The decline was driven by slower hiring, not layoffs. The juniors were not fired. They were never hired in the first place.

Stanford’s Digital Economy Lab found that employment for developers aged 22 to 25 dropped roughly 20% from its late-2022 peak. CS graduate unemployment sits at 6.1%. For ages 22 to 27, it is 7.4%, nearly double the national average.

Big Tech junior hiring fell from 32% of new hires in 2019 to 7% today. Salesforce halted new engineer hiring entirely. Klarna froze developer recruitment. A LeadDev survey found that 54% of engineering leaders plan to hire fewer juniors going forward. AI was the reason given every time.

The math is straightforward. If a senior developer with AI tools produces the output of a senior plus two juniors, the two juniors do not get hired. That is not ideology. It is a spreadsheet.

Camp One: Juniors Are Dead Weight

The efficiency camp does not enjoy saying this out loud, but they are thinking it. A senior engineer with Copilot, Cursor, or Claude Code ships more code in a week than a junior ships in a month. The code is better. It needs less review. It has fewer bugs. The senior already knows the architecture, the edge cases, the patterns. The AI compresses the implementation. The junior was the implementation.

Dario Amodei, the CEO of Anthropic, said it plainly: entry-level jobs are squarely in the crosshairs. He predicted AI could eliminate 50% of entry-level white-collar jobs within five years. He said leaders were sugar-coating what was coming.

Companies report 40 to 55 percent more code output per sprint after adopting AI coding tools. The velocity gains are real. The developer grind that produces them is relentless. And most of that velocity came from eliminating the work that juniors used to do: the boilerplate, the CRUD endpoints, the straightforward implementations that a senior would spec and a junior would type.

This camp will tell you that the market is correcting. That the industry over-hired juniors for years because implementation was expensive. That AI made implementation cheap. That hiring someone to do cheap work at expensive wages is not sustainable. That this is not cruel. It is math.

Camp Two: We Are Eating Our Seed Corn

The pipeline camp has one question the efficiency camp cannot answer: where do future seniors come from?

Matt Garman, the CEO of AWS, called replacing juniors with AI one of the dumbest things he has ever heard. His argument: they are your cheapest employees, your most AI-engaged learners, and if you stop hiring them now, you will have nobody who learned anything when you need senior engineers ten years from now.

Microsoft’s Mark Russinovich and Scott Hanselman published a paper in the Communications of the ACM warning that companies are optimizing for short-term throughput while the pipeline that produces future seniors quietly collapses. Their projection: a reduction in junior hiring between 2024 and 2026 means a proportional senior shortage between 2031 and 2036.

Fast Company ran a piece documenting what happens when you eliminate juniors: burnout. Senior engineers expected to absorb junior workloads plus manage AI tools are burning out faster than companies can replace them. The seniors who were supposed to benefit from not having juniors to mentor are drowning in the work juniors used to share.

This camp will tell you that senior engineers do not appear from nowhere. They are built. Through years of shipping bad code and learning from the failure. Through debugging production incidents at 2am and understanding why the shortcut they took six months ago is the reason they are awake. Through code reviews where a more experienced developer explained not just what was wrong but why it mattered.

Remove that process and you do not get a more efficient industry. You get an industry with a ten-year expiration date on its talent supply. The security consequences of that talent gap are already visible.

The Learning Paradox Nobody Talks About

There is a deeper problem underneath the hiring numbers. AI does not just replace junior work. It replaces junior learning.

Anthropic’s own research found a 17-point comprehension gap between developers who learned with constant AI assistance versus those who coded manually. Debugging skills showed the largest deterioration. The developers who used AI the most understood the least about what they built.

A randomized trial by METR found that experienced open-source developers using AI were 19% slower on average but believed they were faster. If experienced developers misjudge their own productivity with AI, imagine what happens to someone who has never worked without it.

The junior developer who never struggled through a bad database schema never learned why normalization matters. The one who never optimized a slow query by hand never developed intuition for what the database is actually doing. The one who never shipped without tests and got paged at 2am never internalized why testing is not optional.

The pain was the education. Remove the pain and you remove the education. What remains is someone who can prompt an AI to produce code they cannot evaluate, debug, or explain. That is not a junior developer. That is an operator who does not understand the machine they are operating. And when their code passes through a review process that has become a rubber stamp, there is no safety net left.

The Trust Problem

Research shows that junior developers trust AI output at 78%. Seniors trust it at 39%. That gap is not about skepticism versus enthusiasm. It is about pattern recognition.

A senior developer has seen enough production failures to know where AI tends to go wrong. They have intuitions about security, performance, and maintainability that were built through years of consequences. When the AI produces something that looks clean but has a subtle flaw, the senior catches it because they have been burned by that class of flaw before.

A junior developer does not have that library of failures. They cannot evaluate what they cannot contextualize. They accept the output because they have no basis for skepticism. The code compiles. The tests pass. The PR gets approved. The flaw ships to production and nobody knows until a customer finds it.

The paradox: AI is most dangerous in the hands of the people most likely to trust it completely. And the people most likely to trust it completely are the ones with the least experience to validate what it produces.

Neither Camp Has a Plan

The efficiency camp has no answer for the pipeline problem. You cannot run an industry on seniors forever. They retire. They burn out. They leave for management or consulting or farming or whatever people do when they are tired of being on call. If nobody replaces them, you do not have a talent shortage. You have a talent extinction.

The pipeline camp has no answer for the economics. Telling companies to hire people they do not need for work that can be automated is not a workforce strategy. It is charity with a training label. No CFO is going to approve headcount for someone whose primary value is that they might become useful in seven years.

The real answer is probably somewhere in the middle, and neither camp wants to hear it. The junior developer role as it existed from 2010 to 2022 is gone. The work that defined it, the boilerplate and the basic implementations, is automated. That specific job is not coming back.

But the need for people who are learning to become senior engineers has not gone away. The role has to change. What that looks like, whether it is apprenticeships, preceptorships like Microsoft proposed, or something entirely new, nobody has figured out yet. The industry is moving too fast to stop and think about it, which is exactly why it needs to stop and think about it.

The Question Nobody Wants to Ask

If you are a hiring manager: look at your team in five years. Where do your senior engineers come from? If the answer is other companies, ask yourself what happens when every company has the same plan.

If you are a junior developer: the career path that existed for the generation before you is gone. That does not mean there is no path. It means the path has not been built yet and you might have to help build it. The developers who figure out how to combine AI tools with genuine understanding, not just prompting but actual comprehension, will be extraordinarily valuable. The ones who use AI as a crutch instead of a tool will be the ones the efficiency camp points to when they justify not hiring the next cohort.

If you are a senior developer: the juniors you mentor today are not slowing you down. They are the reason someone will be available to maintain your systems after you move on. The thirty minutes you spend on a code review teaching a junior why their approach has a hidden performance problem is not lost productivity. It is the only investment that produces senior engineers.

The junior developer is not dead. But the junior developer role as we knew it is. And the industry has about five years to figure out what replaces it before the consequences become impossible to reverse.

System prompts are not a security boundary for AI agents

AI agents are moving from generating text to taking actions.

They can run commands, send emails, issue refunds, update records, call internal tools, and touch production workflows.

That changes the security model.

A system prompt can guide an agent, but it should not be the thing that enforces policy.

If an action has a real side effect, there should be a control point before that action happens.

The problem

When an agent calls a tool, the important event is not the text the model generated.

The important event is the tool call.

That is where something real can happen.

A model can be manipulated.
A model can hallucinate.
A user can ask for something risky.
A prompt can be ignored or misunderstood.

So the question should not only be:

Did the model intend to do this?

It should also be:

Should this action be allowed under company policy?

A simple example

Imagine a support agent that can issue refunds.

A prompt might say:

Only issue refunds when appropriate.

That is useful guidance, but it is not enforcement.

A better pattern is to check the action before the refund tool executes.

For example:

Refund under $100: allow
Refund between $100 and $500: require approval
Refund over $500: block

Now the rule is not just hidden inside the prompt.

It is enforced before the tool callback runs.

What Enforra does

I’m building Enforra as an open source SDK for AI agent runtime control.

It sits before your tool callbacks and returns a decision before anything executes:

allow
block
require_approval
log_only

The application still owns the actual tool execution.

Enforra does not run your tools remotely.

It gives your app a policy decision before the callback is called.

Why this matters

As agents move into production, teams need more than prompt instructions and logs after the fact.

They need clear policy checks around actions that matter.

That becomes important when agents can touch money, customer data, internal systems, production infrastructure, or business workflows.

The goal is not to make agents less useful.

The goal is to make sure useful agents have a clear control point before they do something risky.

Open source

The initial Enforra SDK is here:

https://github.com/enforra/enforra

NIS2 Article 21 in Azure: Implementing Network Security Controls with Terraform

NIS2 Article 21 in Azure: Implementing Network Security Controls with Terraform

Tags: terraform, azure, security, devops

Canonical URL: https://woitzik.dev/blog/nis2-article-21-azure-terraform

Most NIS2 content is written by lawyers for lawyers. This article maps Article 21 network security requirements to concrete Azure resources — with Terraform code, not legal theory.

Disclaimer: This covers technical network infrastructure controls relevant to NIS2 Article 21. Full compliance requires organizational measures beyond infrastructure code. Nothing here constitutes legal advice.

What NIS2 Article 21 Actually Requires (For Engineers)

Four concrete network security areas:

  1. Network Segmentation — isolate systems by criticality
  2. Access Control — Default-Deny at network and identity layer
  3. Minimize Attack Surface — eliminate unnecessary public endpoints
  4. Auditability — traceable, version-controlled infrastructure changes

Here’s how each maps to Azure Terraform resources.

Control 1: Network Segmentation via Hub & Spoke

Spoke-to-Spoke traffic blocked by default — all cross-workload communication routes through the Hub:

resource "azurerm_virtual_network_peering" "hub_to_spoke1" {
  name                      = "peer-hub-to-spoke1"
  virtual_network_name      = azurerm_virtual_network.hub.name
  remote_virtual_network_id = azurerm_virtual_network.spoke1.id
  allow_forwarded_traffic   = true
}

Spokes only peer with the Hub — never with each other. Cross-Spoke traffic must be explicitly routed through the Firewall.

Control 2: Default-Deny NSG Baseline

security_rule {
  name                       = "Allow-VNet-Inbound"
  priority                   = 100
  access                     = "Allow"
  source_address_prefix      = "VirtualNetwork"
  destination_address_prefix = "VirtualNetwork"
}

security_rule {
  name                       = "Deny-Internet-Inbound"
  priority                   = 4096
  access                     = "Deny"
  source_address_prefix      = "Internet"
  destination_address_prefix = "*"
}

Priority gap (100 → 4096) leaves room for application rules without restructuring the baseline.

Control 3: Egress Control via Forced Tunneling

All outbound Spoke traffic through Azure Firewall — logged, inspectable, FQDN-controlled:

route {
  name                   = "route-to-firewall"
  address_prefix         = "0.0.0.0/0"
  next_hop_type          = "VirtualAppliance"
  next_hop_in_ip_address = azurerm_firewall.fw.ip_configuration[0].private_ip_address
}

# Critical — without these, Windows VMs lose activation
route {
  name           = "bypass-azure-kms"
  address_prefix = "23.102.135.246/32"
  next_hop_type  = "Internet"
}

The Firewall log is your audit trail for NIS2 Article 21(2)(b) — monitoring and incident detection.

Control 4: Zero Public PaaS Endpoints

resource "azurerm_key_vault" "kv" {
  public_network_access_enabled = false

  network_acls {
    default_action = "Deny"
    bypass         = "AzureServices"
  }
}

An external scanner gets no TCP connection — the endpoint doesn’t resolve to a public IP. Directly satisfies NIS2 Article 21(2)(h).

Control 5: No Static Credentials

Managed Identity + scoped RBAC — no connection strings, no access keys:

resource "azurerm_role_assignment" "app_kv_secrets" {
  scope                = azurerm_key_vault.kv.id
  role_definition_name = "Key Vault Secrets User"
  principal_id         = azurerm_linux_function_app.app.identity[0].principal_id
}

When the resource is deleted, identity and permissions are automatically destroyed.

Control 6: Auditability via IaC

Every change is a Git commit. terraform plan is an audit artifact. No ClickOps, no undocumented Portal changes.

What This Covers — and What It Doesn’t

This addresses technical network controls only. Full NIS2 compliance also requires incident response procedures, supply chain risk management, business continuity measures, and organizational governance.

Infrastructure code is the foundation. Compliance is the full building.

Full article with complete code for all six controls on my blog.

👉 Full article on woitzik.dev

How I Built My Own Laravel Analytics Package (and Almost Didn’t Crash Production)

Why Not Google Analytics? (Or why I love reinventing the wheel)

To be honest, this wasn’t an easy call. I’ve been in development for quite a while and had grown accustomed to GA. What could be simpler: you slap a script with your ID onto the site, and data starts flowing into the analytics console. All that’s left is to wait for the traffic to roll in and then analyze it by country, gender, age groups, and so on.

Yes, that’s how it used to be, but in today’s reality, there are objective reasons to rethink this concept. Let’s be real: hooking up Google Analytics in 2026 is like putting a massive deadbolt on your front door but leaving the keys with your neighbor. Everything seems under control, but the neighbor knows exactly when you came home, what you bought, and why you have a long face. And when someone visits you and wants to stay incognito, the neighbor won’t give them the keys, and they won’t even tell you they stopped by. Their response would be something like: “Nobody came, I never sleep, everything is under control…”. What am I getting at?

Remember the General Data Protection Regulation (GDPR)?

Now, for your analytics to work, you must display a cookie consent banner and get the user’s permission. And that’s where the problem lies. 90% of users don’t accept all cookies—only the strictly necessary ones. And what does that mean? Google Analytics ends up dead in the water. Besides, everyone is sick and tired of these banners. So, if there’s a legal way to ditch them, why not take it?

(To learn how to avoid legal trouble with data protection laws, check out the article GDPR Without the Headache: A Guide for Web Developers in Germany )

The decision to drop GA was made. When I decided to hit the “Eject” button and catapult myself out of the Google ecosystem, I faced a logical question: what would fill the void? Because I still needed the numbers. I started looking at the heavy artillery.

The first candidate was Matomo (formerly Piwik): Probably the most powerful all-in-one machine. It’s like keeping a pet elephant in your backyard. It does everything, the database grows like crazy, but it requires a separate PHP server, MySQL, and constant babysitting. For my small pet projects, it felt like trying to drive nails with a microscope.

The second tool I looked at was Plausible / Fathom: Sleek, modern, and privacy-respecting. But there’s a catch: you either pay a subscription (a questionable investment for a free tool) or you mess around with self-hosted Docker versions, which also eat up a good chunk of your VPS RAM.

I looked at all of this and thought: “Do I really need to spin up an entire infrastructure just to know that 50 people read my article on German taxes yesterday?”. That’s when it hit me: I don’t need a “combine harvester.” I need a tiny, precision scalpel that lives right inside my Laravel application.

I wanted something of my own: lightweight, like a morning espresso, and not asking annoying questions about GDPR. Plus, I was simply curious about who all these people (and bots) were “knocking” on my security tools at oleant.net. Since this blog also needed the same tool, I decided to develop a standalone package that could be easily installed via composer and published openly on Packagist.

For those interested in diving into the code, the package is called oleant/laravel-visit-analytics, compatible with Laravel versions 10/11/12. GitHub link: https://github.com/Oleant-NET/laravel-visit-analytics

The Heart of the System: Middleware and the Magic of the Aftertaste

Which architecture to use? There are various ways to implement this. Fortunately, Laravel has a great Middleware mechanism. I decided to stick with that, but would it slow down the user? They shouldn’t have to care about my overhead. And what if the database goes down or something goes wrong? A 500 error page as the face of the site is definitely not what I was aiming for.

That’s why a crucial decision was made — to use the terminate() method. In a Laravel Middleware, it’s like a polite waiter: he brings you the check and smiles (the handle() method), and only after you’ve already left the restaurant does he go back to wipe the table and log the tip (the terminate() method).

The user has already received their page and is happy, while our server quietly and without rush writes the logs to the database in that moment. Even if something goes wrong, the client still leaves satisfied, and the waiter… just doesn’t get a tip this time. Just kidding, but here’s how it works in practice:

PHP

public function terminate(Request $request, Response $response): void
{
    try {
        // Logic to exclude non-target clients, like Admin etc.
        // …
        // Next, write our visitors to the database
        $this->logVisit($request);
    } catch (Throwable $e) {
        // If the DB takes a nap, we won't wake the user with a 500 error
        Log::error("Analytics failed, but we're keeping our cool: " . $e->getMessage());
    }
}

Lifehack: All analytics code must be wrapped in a try-catch. Believe me, there’s nothing sillier than “crashing” an entire project just because the logger didn’t have enough room for a long User-Agent or some other non-obvious case.

GDPR on the Fly: How Not to Become Public Enemy No. 1
To avoid slapping a banner the size of half the screen saying “We are watching you, bro,” I implemented IP anonymization. We simply trim the last part of the address before it ever touches the database. This anonymizes the user and fully complies with the law. Yet, we can still tell which country, data center, etc., the visit came from. We’ll talk more about data center bots once there’s more data to show.

Back to IP anonymization:

PHP

// Before: 192.168.1.154 -> After: 192.168.1.0

It’s like seeing a crowd in masks: you understand that 5 people showed up, but who among them is your neighbor — you have no clue. The law is satisfied, and so is my conscience.

Payload: Gathering Only the Goodies
Initially, I wanted to write everything that comes in the URL to the database. But then I looked at the Livewire logs, where half the state of the planet is passed in the parameters, and realized — the database would explode. So, I decided to implement filtering based on allowed parameters in the config using array_intersect_key. Now, only what I’ve personally authorized in the config ends up in the logs. Clean, orderly, and zero fluff.

The default set in the package config looks like this:

PHP

'whitelist' => [
    'utm_source',
    'utm_medium',
    'utm_campaign',
    'utm_term',
    'utm_content',
    'ref',
],

But you can, of course, change it by publishing the config in your project first:

Bash

php artisan vendor:publish --tag="visit-analytics-config"

After that, a visit-analytics.php file will appear in your config folder. There, you can not only expand the list of tracked parameters (for example, adding something like page or search) but also specify excluded paths so you don’t turn your database into a dump for admin panel or technical endpoint logs.

What’s Next?

The package started humming, and the data began to flow. I closed my laptop and went to bed, thinking I’d wake up to some visitor charts from a couple of friends. But reality turned out to be much tougher (and more interesting).

In the next episode, we’ll step through the “looking glass”: who are Palo Alto Networks, why do bots read my Terms of Service in 7 seconds, and why Linux + DuckDuckGo is a badge of quality for a visitor?

What is Microsoft E7 and Do You Need It?

If your organization is running AI agents, the answer is probably yes.

Microsoft 365 E7, also called the Frontier Suite, became available on May 1, 2026 at $99/user/month on an annual commitment. It bundles four products into one SKU: Microsoft 365 E5, Microsoft 365 Copilot, the Microsoft Entra ID Suite, and Microsoft Agent 365. Buy them separately and you’re looking at roughly $117/user/month. The bundle saves you about $18 per user, or around 15%, before any CSP promotions apply.

The first three components won’t surprise anyone who’s been watching Microsoft licensing. E5 is the existing enterprise security and productivity foundation. Copilot is the AI assistant most organizations have already been pricing as a standalone add-on since 2024. Entra ID Suite adds the deeper identity governance features – Privileged Identity Management, Identity Protection, lifecycle workflows – that E3/E5 customers typically bolt on separately.

Agent 365 is the piece most IT teams haven’t budgeted for yet. That’s the one worth paying attention to.

Why Agent 365 Changes the Equation
AI agents are already in production at most large organizations, whether IT knows about it or not. IDC projects 1.3 billion AI agents in use by 2028, and 80% of the Fortune 500 are already running Microsoft’s agentic capabilities. The problem isn’t adoption – it’s governance. Agents that spin up without proper oversight become a shadow-IT problem fast: they access data, generate outputs, and operate under policies nobody set.

Agent 365 gives each AI agent an identity in Entra, tracks what it accesses, and enforces your existing compliance and security policies on it the same way those policies apply to people. If you’re running Copilot Studio workflows, Foundry automations, or any autonomous process that touches sensitive data, this is the control plane you need. Without it, you’re essentially running agents with no audit trail.

You can buy Agent 365 as a standalone for $15/user/month. But if you’re already on E5 with Copilot, you’re spending $117/user combined – at which point E7 at $99 just makes more sense.

The Promotions
Through December 31, 2026, Microsoft is offering CSP customers introductory discounts on E7:

  • 10% off with 10+ seats on an annual term (~$89.10/user/month)
  • 15% off with 100+ seats on an annual term (~$84.15/user/month)
  • 15% off with 300+ seats on a three-year term (~$84.15/user/month)

At 100+ seats with the 15% promo, you’re paying about $84/user instead of the $117 you’d pay stitching it together yourself. For a 150-user org, that’s roughly $59,000 in annual savings compared to separate licenses.

Who Should Skip It
E7 isn’t for everyone. If you’re on Business Premium and only want Copilot, the price jump doesn’t work out. If Entra ID P1 covers your identity needs and you have no agents in production or on the roadmap, you’re paying for governance tooling you won’t use. The trap we see most often is organizations upgrading to E7 for Copilot alone, then leaving Agent 365 dormant. If that’s your plan, stay on E5 plus standalone Copilot and revisit at renewal.

But if agents are already running in your environment – or they will be in the next 12 months – E7 is the licensing structure built for it. The governance capability isn’t a nice-to-have at that point. It’s the thing that keeps autonomous AI from becoming your next compliance problem.

Find out more in this in-depth breakdown of M365 E7.

NeuroCalT+ Mental Math

A long time ago, I read a perfect book. I really like that kind of stuff. The main problem I had back then was trying to train my skills. It is so much fun to use this method to solve math problems, but it is absolutely not as fun to create those tasks for yourself. I thought a lot about how to do it and tried several apps, but they didn’t offer what I wanted. I wanted pure math—no gamification, no ads, just examples to solve.

Moreover, I really like multiplication by 5 (even numbers). It is so simple and so impressive when you show someone how you can multiply any number like

## 84848284868682 * 5

in seconds. I am glad I met people who are interested in it as well. The story of Trachtenberg is unique, and that’s a fact.

I decided to make a small app for training, and I found that the most interesting part is multiplication. Maybe I will add some other operations in the future. If you want, you can try it out. It is free, requires no registration, has no annoying ads, and so on—just pure math. I hope you like it!

https://play.google.com/store/apps/details?id=com.milvus.grema

Şipşak özelliği kapatma! İnstagram'da şipşak nasıl ve nereden kapanır?

Şipşak özelliği kapatma! Instagram’ın mesaj kutusuna entegre ettiği “Şipşak” özelliği, kullanıcıların anlık fotoğraflarını hızlı şekilde paylaşmasına imkan tanırken sosyal medya kullanıcılarının gündeminde yer aldı. İnstagram şipşak özelliği kapatma! Snapchat benzeri yapısıyla dikkat çeken sistemde paylaşımlar kısa süre içinde kaybolurken, ekran görüntüsü alınamaması da kullanıcılar arasında merak edilen başlıklar arasında yer aldı.

What did our VPC look like at 22:14 (Tue, 2026-05-20) — building clew, a CLI for navigating AWS Config snapshots

clew data flow: AWS Config snapshot to DuckDB to render/query outputs

It’s 2am. Production is on fire. Someone in the war room asks, “what did the VPC actually look like at 22:14, when this started?”, and the only honest answer is, “give me twenty minutes with jq.”

That moment, repeated enough times, is why I started writing the tool I want to talk about today: clew, a Go CLI that ingests AWS Config Configuration Snapshots into a local DuckDB and renders an interactive topology you can actually read.

▎ https://github.com/nishikawaakira/clew

This post is the story of why it exists and how to use it during an incident.

The problem: “the moment-in-time configuration” is a real artifact you can’t easily see

A scenario you might recognize:

It’s 22:14. Targets behind an ALB start going UnHealthy, but only some of them. CloudWatch shows the symptom but not the cause. People in the war room start floating theories:

  • “Maybe someone tightened a security group rule.”
  • “Maybe a route table was modified and Private Subnet traffic is now going out the wrong gateway.”
  • “Maybe a subnet’s route table association silently changed.”

All plausible. But each of them needs the same piece of evidence: what the topology actually looked like at 22:14, ideally next to what it looked like at 18:00 when things were still fine. The AWS Console only shows you “right now,” which is precisely the moment you don’t care about anymore.

The good news: AWS Config has been quietly dropping a full Configuration Snapshot of every tracked resource into S3 every six hours. The artifact exists. The bad news is that the artifact ships as:

  • A <account>_Config_<region>_ConfigSnapshot_<timestamp>_<snapshot-id>.json.gz file
  • Containing a flat configurationItems array, mixed across resource types
  • Where “this EC2 uses this security group” lives under configuration.networkInterfaces[].groups[].groupId
  • And “this VPC contains these subnets, which contain these instances” is purely implicit — you reconstruct it in your head from the relationships[] field

jq at 2am is a poor incident-response interface. And drawing the VPC topology on paper while pagers are going off is worse.

Existing tooling, and where it fell short for me

I’m not pretending nothing else exists. The usual suspects:

  • AWS Config Aggregator + Advanced Queries (SQL-ish querying from the console)
  • CloudFormation StackSets with drift detection
  • Commercial CMDB / IaC visualizers (Lucidchart’s AWS importer, Hava, etc.)
  • Rolling your own jq library and committing it to the team’s snippet repo

What I couldn’t find was something that hit all of:

  1. Works against an arbitrary historical snapshot (any S3 object, not just “now”)
  2. Local-first, no SaaS round trip — incident artifacts shouldn’t leave the responder’s laptop
  3. Produces a single self-contained HTML file I can paste into a Slack thread or a postmortem doc
  4. Lets me stack multiple snapshots in the same store and compare across time

So I started writing what I wished existed.

What clew does

Three commands, one data model:

clew data flow: AWS Config snapshot to DuckDB to render/query outputs

The DuckDB file holds three tables:

config_items

Stores one row per imported configurationItem, including the original configuration, relationships, tags, and capture_time.

graph_nodes

Stores one row per resource, including placeholder rows for resources referenced by relationships but not yet imported.

graph_edges

Stores resource-to-resource edges extracted from both relationships[] and the configuration body.

clew VPC topology view showing VPC, subnet, EC2, and ENI nodes

The default render --format html output is the part most worth showing off. It produces an interactive topology with compound nodes — VPCs are drawn as outer boxes containing subnet boxes containing EC2 / ENI nodes — the same nesting you’d draw on a whiteboard during incident response, except generated from the snapshot you just imported.

The generated HTML is interactive: you can pan and zoom, click nodes to inspect their type, resource ID, ARN, and placeholder status, toggle edge labels, switch layout direction, and choose between orthogonal and bezier edges.

The file is fully self-contained — copy it anywhere, open it in any browser.

Walkthrough: using clew in incident-response mode

Step 0: Install

# Requires Go 1.24+ and a C compiler — go-duckdb is CGO-bound.
go install github.com/nishikawaakira/[email protected]

This assumes AWS Config is already enabled and delivering configuration snapshots to S3.

Step 1: Pull the snapshot(s) closest to the incident from S3

AWS Config writes keys with unpadded month/day (…/2026/5/20/…), so dancing around BSD-vs-GNU date is a waste of energy. Listing recursively and filtering by name is the safer path:

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=<incident-region>
BUCKET=<your-config-bucket>

aws s3 ls "s3://${BUCKET}/AWSLogs/${ACCOUNT_ID}/Config/${REGION}/" --recursive | grep ConfigSnapshot | sort

# Grab the two snapshots straddling the incident window and `aws s3 cp` them down.

If you need one right now and don’t want to wait for the next scheduled delivery:

aws configservice deliver-config-snapshot --delivery-channel-name default

In my experience, the S3 object usually shows up within 10–60 seconds.

Step 2: Import everything into a single DuckDB

clew import --input snapshot-2026-05-20T22-00.json.gz --db incident.duckdb
clew import --input snapshot-2026-05-20T18-00.json.gz --db incident.duckdb

You can keep stacking snapshots into the same incident.duckdb. config_items.item_id is a PRIMARY KEY of account:region:type:resource_id:captureTime, combined with ON CONFLICT (item_id) DO NOTHING on insert.

The practical guarantees:

  • Re-importing the same snapshot is a true no-op (the inserts collide on item_id).
  • Re-importing a snapshot taken at a different capture time keeps both rows for the same resource — history is preserved, not overwritten.

graph_nodes / graph_edges always reflect the latest state, so the rendered topology is always “the most recent snapshot you imported,” but the historical detail still lives in config_items.

Step 3: Render the topology

clew render --db incident.duckdb --view vpc --format html --output vpc.html
open vpc.html

If you want it for a Slack thread:

clew render --db incident.duckdb --view vpc --format png --output vpc.png

The raster output is fine for a quick share, but it doesn’t scale well to large topologies — once you have more than a couple of dozen resources, the nodes get cramped and labels become hard to read. For anything larger, the interactive HTML is the better artifact to share with the team, because reviewers can open it locally and pan/zoom.

If you want to embed it in a postmortem Markdown:

clew render --db incident.duckdb --view vpc --format mermaid --output vpc.md

Step 4: Zoom in on a suspect

When you have a specific theory — “the ALB stopped reaching this instance” — the global view is too much. query runs a bidirectional BFS (breadth-first search) from one resource:

# Two hops out from a specific instance, as interactive HTML.
clew query --db incident.duckdb --resource-id i-0abc... --depth 2 
 --format html --output i-0abc.html

# Or a text summary in the terminal when you just want to confirm a hunch.
clew query --db incident.duckdb --resource-id sg-12345 --depth 1 --format text

The text format is intentionally boring and grep-friendly:

Nodes:
  - AWS::EC2::Instance/i-0abc...
  - AWS::EC2::SecurityGroup/sg-12345
  - AWS::EC2::Subnet/subnet-pub
  ...
Edges:
  - <node-id> ==[Uses security group]==> <node-id>
  ...

Step 5: Diff across time, with plain SQL

Because every snapshot lands as its own row in config_items, you can ask DuckDB directly:

duckdb incident.duckdb
SELECT capture_time, configuration_json
FROM config_items
WHERE resource_id = 'sg-12345'
ORDER BY capture_time;

Comparing the 22:00 vs. 22:14 configuration_json of a single security group is now a diff between two rows. The whole point of putting the data in a real database was to make the ad-hoc questions stop requiring custom code.

What clew doesn’t do yet

Being honest about scope:

  • Only the vpc view is implemented. The internal model is type-agnostic, so adding iam, org, kms, or network (peering/TGW) views is a matter of writing the type list and the configuration-edge extractors, not a redesign.
  • The HTML output pulls Cytoscape.js / dagre / cytoscape-dagre from a CDN at view time. Useful in 95% of cases, useless on a fully air-gapped laptop. An –embed-js mode using go:embed is the obvious follow-up.
  • No special tuning for very large snapshots. Inserts are straightforward prepared statements. For tens of thousands of resources you’d want the DuckDB Appender API or parallel ingest.
  • Cross-account / cross-region edges only exist if AWS Config explicitly listed them. Reconstructing organization-wide topology needs more wiring (likely an Organizations + Config Aggregator integration).

Why “clew”?

clew is the Old English word for “ball of thread” — and the literal etymological root of the modern word clue. In Greek mythology it’s the thread Ariadne gave Theseus so he could find his way back out of the Labyrinth.

Production AWS environments tend to drift toward labyrinth shape, especially after years of “just one more security group / one more subnet / one more peering connection.” Most of the time it’s fine. Then 2am happens, and the same environment that felt familiar yesterday looks like a maze.

clew is named for the thread you wish you had on the way in.

Try it

go install github.com/nishikawaakira/[email protected]
clew --help

Repository: https://github.com/nishikawaakira/clew (MIT)

If you want to see the output without setting up real AWS Config, the repo ships a testdata/sample_snapshot.json that exercises every piece of the rendering pipeline:

clew import --input testdata/sample_snapshot.json --db /tmp/demo.duckdb
clew render --db /tmp/demo.duckdb --view vpc --format html --output /tmp/demo.html
open /tmp/demo.html

This is still a PoC — there are sharp edges and obvious gaps. Issues, PRs and “here’s the view I actually need” feature requests are all welcome.

Thanks for reading. If this means one fewer engineer is writing jq queries at 2am next month, the project has paid for itself.