{"slug": "claude-code-properly-wired-a-one-stop-guide-to-spec-driven-development-on-a-real", "title": "Claude Code, Properly Wired: A One-Stop Guide to Spec-Driven Development on a Real Project", "summary": "A guide to spec-driven development with Claude Code, executed on a Flask + SQLite expense tracker called Spendly, demonstrates how to wire slash commands, subagents, skills, hooks, and a CLAUDE.md file to make AI-assisted development reliable. The repository includes 6 subagents, 7 slash commands, 2 skills, 3 Python hooks, 12 specs, and 252 tests, with the .claude/ directory as the deliverable.", "body_md": "*Everything in this guide was executed on one repository — a Flask + SQLite expense tracker called Spendly — from empty **.claude/ directory to a live app on AWS EC2 behind HTTPS. Every number, screenshot, and command is from that run. Nothing is illustrative.*\n\nClaude Code becomes reliable when we stop prompting it and start wiring it. A slash command orchestrates subagents; subagents load skills for domain knowledge; hooks enforce invariants deterministically outside the model’s judgment; and a CLAUDE.md holds the facts every session needs. The discipline that makes this work is Spec-Driven Development — write the contract first, review it, then build against it, because a spec is the only artifact that both we and the model can hold each other to.\n\nSpendly is deliberately boring: a personal expense tracker, Flask, SQLite, Jinja2 templates, vanilla JS, no build step. The app is the *vehicle*. The .claude/ directory is the *deliverable*.\n\n```\nspendly/├── app.py                    # every route, single file, no blueprints├── database/│   ├── db.py                 # connection + schema + users│   └── queries.py            # expense + profile reads/writes├── templates/                # base.html + one file per page├── static/css/               # style.css global; one file per page otherwise├── tests/                    # pytest — 252 tests├── requirements.txt          # dev + test├── requirements-prod.txt     # -r requirements.txt + gunicorn├── Dockerfile                # phase 1├── .dockerignore├── compose.yaml└── deploy/vm/                # phase 2 — nginx, systemd, bootstrap, RUNBOOK\n.claude/├── CLAUDE.md  (project root, actually)   # always-on project facts├── agents/          6 subagents│   ├── spendly-test-writer.md            spec → tests│   ├── spendly-test-runner.md            run + diagnose│   ├── spendly-security-reviewer.md      auth, injection, exposure│   ├── spendly-quality-reviewer.md       naming, placement, idiom│   ├── spendly-devops-engineer.md        builds deploy artifacts│   └── spendly-devops-reviewer.md        audits them, read-only├── commands/        7 slash commands│   ├── create-spec.md            spec + branch│   ├── test-feature.md           writer → runner│   ├── code-review-feature.md    security ∥ quality│   ├── deploy-phase.md           engineer → reviewer│   ├── ship-feature.md           commit → PR → merge → cleanup│   ├── seed-user.md│   └── seed-expense.md├── skills/          2 skills│   ├── spendly-devops/│   │   ├── SKILL.md              router + phase 0 + invariants│   │   └── references/           phase-1-docker, phase-2-cloud-vm,│   │                             phase-3-kubernetes, cicd│   └── spendly-ui-designer/SKILL.md├── hooks/           3 Python hooks│   ├── devops_router.py          UserPromptSubmit│   ├── format_python.py          PostToolUse│   └── protect_paths.py          PreToolUse — blocking├── specs/           12 specs, one per feature step├── settings.json                 hooks + permission allowlist (shared)├── settings.local.json           personal overrides (untracked)└── verify_setup.py               56 checks that the wiring is intact\n```\n\nA useful way to understand the repository is to separate the product plane from an agent control plane.\n\nThe product plane says what the software is. The control plane says how an AI worker is expected to interact with that software. This is why the .claude/ configuration deserves the same engineering attention as CI configuration or infrastructure code.\n\nA malformed command can route work incorrectly; a stale skill can produce a bad design; a broken hook can silently stop enforcing a safety rule.\n\nThis also explains why the guide calls the application the *vehicle* and the Claude setup the *deliverable*. The interesting artifact is not a Flask expense tracker; it is a repeatable operating model for an AI-assisted repository.\n\nAlthough most files are Markdown, they are not merely documentation. Their content changes runtime behavior:\n\n```\nHuman request   ↓Claude loads project facts and routing descriptions   ↓Commands/agents/skills shape the task   ↓Hooks may allow, modify, warn, or block actions   ↓Repository or external system changes\n```\n\nThat makes these files closer to policy-as-code than to a wiki page. The syntax is prose, but the operational effect is real.\n\nverify_setup.py therefore plays the role of a lightweight type checker for the agent control plane. It turns hidden coupling into explicit tests. The broader lesson is reusable: whenever an AI workflow depends on filenames, skill names, frontmatter fields, documented routes, or expected tool availability, add a small machine check for those assumptions.\n\nKeep the control plane small, layered, and testable:\n\nThat separation reduces prompt duplication and makes failures easier to diagnose.\n\nThere are two, and they do different jobs.\n\nOurs is short — a working-style preamble:\n\n```\n# Faizul User WorkflowDefault working style:- Prefer actionable engineering outputs- Use markdown tables when comparing- For architecture: include tradeoffs- For troubleshooting: root cause -> fix -> validationWhen coding:- prefer production-grade patterns- secure defaults- readable structure- explain changed filesWhen uncertain:- say assumptions clearly- suggest verification\n```\n\nThat is the right content for a global file: preferences, not facts. It applies whether we are in a Flask repo or a Terraform one.\n\nOurs is 402 lines and ~5,100 tokens, loaded into every session. It carries:\n\n2. State the *why*, briefly. Compare:\n\n❌ Never put DB logic in routes. ✅ Never put DB logic in routes — it belongs indatabase/db.py (connection, schema, users) ordatabase/queries.py (anything touchingexpenses).\n\nThe second version tells an agent *which* file, which is the part it actually needs.\n\n3. Document the traps, not just the rules. The highest-value lines in our CLAUDE.md are the warnings:\n\n```\n- **`app.secret_key` is hardcoded** and `debug=True` is hardcoded in `__main__`.  Fine for local dev, unsafe anywhere else.- **` seed_db()` runs at import time** and creates demo@spendly.com / demo123.  Harmless locally; a working backdoor on any public host.- **There is no CSRF protection.** Known gap — raise it, don't silently add a  dependency for it.- **There is no migration system.** A schema change means hand-editing a live  SQLite file — back it up with VACUUM INTO, never cp.\n```\n\nEach of those saved an agent from a confident wrong move.\n\n4. Every rule needs an escape hatch, stated. We say “no new pip packages” — and then:\n\n```\n**The one sanctioned exception — approved and landed.** Deployment needs a WSGIserver, because app.run(debug=True) exposes the Werkzeug debugger (remote codeexecution). gunicorn==23.0.0 therefore lives in requirements-prod.txt.\n```\n\nWithout that, an agent facing phase 1 has to either break a rule or fail. With it, the decision is already made.\n\n5. Sync it in the same change, always. If a PR adds a route, it updates the route table. Not “later”. We enforce this mechanically — verify_setup.py compares CLAUDE.md's route table against app.py in both directions, so a route in the code but missing from the docs fails the check, and vice versa.\n\nThe project CLAUDE.md behaves like a persistent working-memory preamble. The important design question is therefore not \"what documentation would be nice to have?\" but:\n\nWhat information is so frequently necessary, and so costly to rediscover or get wrong, that it deserves to consume context in every relevant session?\n\nThat framing produces three categories.\n\narchitecture, repository conventions, safety rules, known constraints, route ownership, test commands, and the location of important components.\n\nA long step-by-step deployment procedure is usually better as a skill or reference file that loads only when deployment work begins.\n\nCurrent issue status, temporary debugging notes, one-off hypotheses, or an unfinished migration checklist should normally be tracked in a task/spec/issue rather than being injected into every future session.\n\nEvery always-loaded paragraph competes with source code, tool results, the current specification, and the conversation. This is why concise, accurate project facts are more valuable than encyclopedic documentation. A 20-line warning section containing repo-specific traps can outperform 200 lines of generic best practices because it changes decisions at the exact points where mistakes are likely.\n\nThink of context as a cache:\n\n```\nhigh reuse + high consequence if wrong  → keep warm in CLAUDE.mdlow reuse or large detail               → load on demandshort-lived state                       → keep outside persistent context\n```\n\nIf six agents trust one stale statement, the error is multiplied. This is a form of configuration drift: the code and the agent’s declared model of the code no longer match. The same idea exists in infrastructure management — desired state and actual state diverge — but here the desired state is natural-language knowledge.\n\nThe strongest mitigation is not “remind people to update docs.” It is to make drift observable:\n\nThat is why “sync it in the same change” is more than style. It is a consistency transaction across code and agent knowledge.\n\nA slash command is a markdown file in .claude/commands/. The filename becomes the command. create-spec.md → /create-spec.\n\nFrontmatter configures it:\n\n```\n---description: Create a spec file and feature branch for the next Spendly stepargument-hint: \"Step number and feature name e.g. 2 registration\"allowed-tools: Read, Write, Glob, Grep, Agent, Bash(git:*)---\nYou are a senior developer spinning up a new feature...User input: $ARGUMENTS\n```\n\nThe body is a prompt, not a script. It is instructions to the model, so it can contain conditionals, gates, and refusals in plain English:\n\n```\nIf no argument is provided, stop immediately and say:\"Please provide a phase. Usage: /deploy-phase <0|1|2|3|cicd>\"\n```\n\nallowed-tools is the security boundary. /code-review-feature is scoped to Bash(git diff), Bash(git status), Read, Glob — it structurally *cannot* edit a file, no matter what the diff tempts it to do.\n\nA slash command is useful because it converts a vague natural-language request into a named, repeatable procedure. It is not just a shortcut for a long prompt. It can encode gates, actor selection, allowed tools, preconditions, and the expected shape of the final handoff.\n\nA good command answers five questions:\n\nThis makes a command similar to a lightweight runbook or CI job, except its steps can contain model reasoning.\n\n**Least privilege belongs at the orchestration boundary**\n\nThe allowed-tools field is powerful because it constrains the *capability surface* of the command. If a code-review workflow only needs to read diffs, it should not have generic Bash or Edit access. This follows the same security principle used for IAM roles and service accounts: grant the minimum capability necessary for the job.\n\nThe benefit is not only security. Narrow tools improve reasoning quality because the agent has fewer irrelevant actions available. A read-only reviewer is naturally more likely to report findings than to “helpfully” rewrite the code it is supposed to assess.\n\nAn idempotent workflow can be run twice without creating uncontrolled side effects. For example, a command can:\n\nNot every command can be perfectly idempotent, but designing for safe re-entry is important because agent sessions can fail, hit limits, or be interrupted halfway through a multi-step workflow.\n\n**Natural language is still code-like**\n\nThe body is prose, but operationally it has control flow:\n\n```\nIF no phase argument    STOP with usageELSE IF prerequisite failed    REPORT blockerELSE    SPAWN engineer    WAIT    SPAWN reviewer    REPORT handover\n```\n\nWriting commands with explicit conditions, stop rules, and expected outputs reduces ambiguity. Treat important prompt workflows with the same care we would give a shell script: clear inputs, predictable branches, and observable failures.\n\nEvery session starts with a fixed budget. Understanding what consumes it before we type anything is the difference between a setup that scales and one that chokes.\n\n**What loads automatically, and what does not**\n\nThat table is the whole game. Descriptions are always-on; bodies are on-demand.\n\nWe first wrote five sibling skills — spendly-devops, spendly-docker, spendly-cloud-vm, spendly-kubernetes, spendly-cicd. Measured cost:\n\n```\nspendly-cicd          desc = 711 charsspendly-cloud-vm      desc = 770spendly-devops        desc = 759spendly-docker        desc = 664spendly-kubernetes    desc = 827-------------------------------------TOTAL   3,731 chars ≈ 932 tokens, in EVERY session\n```\n\nNearly a thousand tokens spent in sessions that never touch deployment. Worse, the descriptions were long *because they had to disambiguate from each other* — spendly-kubernetes needed 827 characters listing \"PVC, StorageClass, CrashLoopBackOff, HPA\" purely so it would not collide with spendly-docker.\n\nThe tell that it was wrong: every one of those skills opened with “load spendly-devops first.\" A skill that is not usable until you load a sibling is a *chapter*, not a skill.\n\nThe fix — progressive disclosure. One skill, four reference files:\n\n```\n.claude/skills/spendly-devops/├── SKILL.md                        # router + phase 0 + invariants (281 lines)└── references/    ├── phase-1-docker.md           # 273 lines, loaded only for phase 1    ├── phase-2-cloud-vm.md         # 393    ├── phase-3-kubernetes.md       # 430    └── cicd.md                     # 311\nAlways-on cost:  932 tokens  →  146 tokens      (-786)On-demand body:  unchanged — 1,407 lines still available when needed\n```\n\nThe router’s description no longer disambiguates anything; it only has to recognise “this is infrastructure-shaped.”\n\nWatch item: ourCLAUDE.md grew from ~150 lines to 402 (~5,100 tokens) over the project. That is the next thing worth trimming — probably by moving the \"Adding a new feature\" walkthrough into a skill.\n\nThe context window is not merely a technical limit; it is the agent’s active information budget. The more irrelevant or redundant material occupies it, the less room remains for the actual problem.\n\nThere are three common forms of context waste:\n\nProgressive disclosure is the antidote.\n\nKeep routing metadata small\n\nthen load detail only after the task has been classified.\n\n```\nAlways loaded   └── enough information to choose the right path          ↓On-demand skill   └── enough information to perform the domain workflow          ↓Reference file   └── detailed phase-specific facts and commands\n```\n\nThis is analogous to memory hierarchy in computer systems: keep small, frequently used information close; move bulky, rarely used information behind a lookup.\n\nA short context can still be bad if it contains contradictions. The model must then spend reasoning effort deciding which instruction is authoritative, and may choose the wrong one. Context engineering therefore has two goals:\n\nA useful review question is: *If an agent saw only this paragraph and the code it is about, could it make the intended decision?* If not, the paragraph may be too vague, too stale, or stored at the wrong layer.\n\nA subagent acts as a temporary private scratch space. It can read many files, perform searches, and return a compact result. This is not free — the subagent still consumes compute and tokens — but it keeps the main session from becoming polluted with every intermediate observation.\n\nThe architecture is therefore a fan-out / summarize / fan-in pattern:\n\n```\nMain context   ↓ delegate searchExplore context reads broadly   ↓ summarizeMain context receives conclusions\n```\n\nThis is most useful when exploration is broad but the final decision depends on a small number of findings.\n\nThe two Review gates are the point. Anyone can get a model to write code; the difficulty is knowing whether the code is *the right code*. A spec makes that answerable, because it converts \"does this look good?\" into \"does this satisfy the Definition of Done?\"\n\n```\nSpec → Review → Design → Review → Tasks → Build → Validate\n```\n\nWhy this matters more with an agent than with a human. A human developer who misunderstands a requirement usually produces something visibly odd. An agent that misunderstands produces something *confidently plausible* — correct-looking code solving the wrong problem. The spec is the artifact that makes that detectable, because tests get written from the spec, not from the implementation.\n\nThat last clause is the mechanism. Our spendly-test-writer agent is explicitly forbidden from reading the implementation for test logic:\n\n```\n## Core PrincipleYou write tests based on **feature specifications and expected behavior**, never byreading or reverse-engineering the implementation. Your tests define what thefeature *should* do, serving as a correctness contract.\n```\n\nIf tests are derived from the code, they assert that the code does what it does — which is always true and tells you nothing.\n\n```\ngit pull origin main  →  git checkout -b feature/x  →  git switch feature/x                                    ↓       Spec → Review → Design → Review → Tasks → Build → Validate                                    ↓git commit -m  →  git push origin  →  create+merge PR  →  git branch -d  →  switch\n```\n\nAs for example Step 7 is a useful first end-to-end example because it crosses the route, template, validation, and database layers without adding a new schema.\n\nThe real spec at .claude/specs/07-add-expense.md\n\nsays the existing GET /expenses/add placeholder must become a logged-in GET + POST flow, backed by insert_expense() in database/queries.py. Its validation contract includes a positive numeric amount, one of seven fixed categories, an ISO date, and an optional description. The current implementation in app.py follows that shape: it checks the session, validates each field, calls insert_expense(...), flashes success, and redirects to profile.\n\nRun:\n\n```\n/create-spec 7 add expense\n```\n\n/create-spec does more than create a Markdown file. Its current command definition requires this sequence:\n\n```\ngit status   ↓parse step/title/slug   ↓ensure branch name is free   ↓checkout main + pull   ↓create feature branch   ↓delegate repository research to Explore   ↓write .claude/specs/07-add-expense.md   ↓report branch + spec path\n```\n\nFor this feature, the spec’s most important contract can be summarized as:\n\n```\nRoutes- GET /expenses/add  — logged-in only- POST /expenses/add — logged-in only\nRules- amount > 0- category ∈ seven fixed categories- date must be YYYY-MM-DD- description is optional- DB writes live in database/queries.py- parameterised SQL only- success redirects to /profile\nDefinition of done- logged-out users redirect to /login- valid submission creates a row- invalid amount/category/date re-renders with an error- optional description works- Add Expense navigation is visible\n```\n\nThat is what “configure the spec” means in practice: define behavior, boundaries, files, and a testable finish line before asking Claude to implement.\n\nRead:\n\n```\n.claude/specs/07-add-expense.md\n```\n\nCheck especially:\n\nIf any answer is wrong, edit the specification now. This is cheaper than correcting the same misunderstanding after code and tests exist.\n\nUse Shift+Tab twice to enter Plan mode.\n\nA good implementation-planning prompt is:\n\n```\nRead .claude/specs/07-add-expense.md and CLAUDE.md.Use the built-in Plan/Explore workflow required by the project.Do not edit files yet.\nProduce an implementation plan that maps every Definition-of-Done item to theexact files/functions/templates that need to change. Call out validation anddatabase-isolation risks before implementation.\n```\n\nReview the plan and approve it only when it matches the spec.\n\nAfter leaving Plan mode:\n\n```\nImplement the approved Step 7 plan exactly against.claude/specs/07-add-expense.md.\nKeep scope inside the spec. Reuse existing Flask and database patterns.Do not add dependencies or schema changes. When done, summarize changed filesand map them back to the spec.\n```\n\nThe current codebase confirms the implementation landed in the expected places: app.py, database/queries.py, templates/add_expense.html, profile/navigation templates, and CSS.\n\nRun:\n\n```\n/test-feature 07-add-expense\n```\n\nThe command is intentionally two-stage:\n\n```\nspendly-test-writer   ↓  derives tests from the spec, not implementationwrites tests/test_07_add_expense.py   ↓spendly-test-runner   ↓runs only that feature test file and diagnoses failures\n```\n\nThe current repository contains tests/test_07_add_expense.py, which confirms this feature ended with dedicated executable coverage.\n\nIf tests fail, fix the implementation against the spec, not by weakening the test to match the code. Then rerun /test-feature 07-add-expense.\n\nRun:\n\n```\n/code-review-feature 07-add-expense\n```\n\nThe command first collects tracked and untracked changes, then starts the security and quality reviewers in parallel. If the verdict is CHANGES REQUESTED, approve the action plan explicitly, apply it, then rerun the feature tests and review.\n\nRun:\n\n```\n/ship-feature\n```\n\nThe shipping command:\n\n```\nidentify feature branch   ↓generate conventional commit message   ↓commit   ↓push   ↓create PR   ↓wait for CI checks   ↓squash merge   ↓delete remote branch   ↓pull main   ↓delete local branch\n```\n\nThat is the full SDD loop in executable form:\n\n```\n/create-spec   → human spec review   → Plan mode   → human plan review   → implement   → /test-feature   → /code-review-feature   → fix/retest if necessary   → /ship-feature\n```\n\nThis is the one spec that we captured and configure end to end through Claude setup.\n\nThis case is the cleanest reference because both the article and the current repository retain the spec, dedicated tests, security finding, review evidence, and merged-PR screenshots.\n\n1. Create the branch and spec\n\n```\n/create-spec 10 export expenses csv\n```\n\nThe generated .claude/specs/10-export-expenses-csv.md is unusually valuable because it records decisions that a generic implementation prompt would likely miss. Our task is just run the /command of create-spec :\n\n```\nRoute- GET /expenses/export — logged-in CSV attachmentKey rules- user_id is required by the query helper and enforced in SQL- never accept user_id from the request- reuse the existing date-filter behavior- return raw amount/date values, not formatted display strings- do not reuse get_recent_transactions() because it formats values and has limit=10- order by date DESC, id DESC- write CSV in memory with csv.writer + io.StringIO- no temporary file\n```\n\nSelected Definition-of-Done items make the contract concrete:\n\n```\nlogged out → 302 /loginlogged in → 200 text/csv attachmentheader row presentraw numeric amount + ISO datemore than 10 expenses → all exportedUser A never sees User B's rowstwo-sided date filter matches profileinvalid/one-sided dates follow existing profile behaviorempty description → empty fieldzero rows → header-only CSVverify_setup.py passes\n```\n\n2. Review the spec before code.If require we can edit accrding to our requirements.\n\nwe should not skip the subtle decisions:\n\n3. Plan in read-only mode\n\nUse Shift+Tab twice, then:\n\n```\nRead .claude/specs/10-export-expenses-csv.md and CLAUDE.md.Use the built-in Plan agent and delegated repository research.\nPlan the smallest implementation that satisfies every DoD item.Pay special attention to:- raw vs display-formatted values,- date-filter parity with /profile,- user ownership scoping,- CSV response headers,- testability.\nDo not edit files yet.\n```\n\n4. Implement only after plan approval\n\n```\nImplement the approved Step 10 plan.Treat .claude/specs/10-export-expenses-csv.md as the contract.Do not widen scope.\nUse the project UI skill for the profile link styling and preserve existingdatabase-layer boundaries. Summarize changed files when complete.\n```\n\n5. Generate and run spec-derived tests\n\n```\n/test-feature 10-export-expenses-csv\n```\n\nThe repository retains tests/test_10_export_expenses_csv.py. In the captured run, the writer produced 25 test functions / 29 cases and the runner mapped them back to the DoD.\n\n6. Run parallel review\n\n```\n/code-review-feature 10-export-expenses-csv\n```\n\nThis is where the security reviewer found the issue that a purely functional test suite could miss: spreadsheet formula injection in CSV free-text cells.\n\nThe fix added _csv_safe() in app.py. The current code confirms the mitigation: leading =, +, -, or @ after left-trimming is prefixed with an apostrophe before CSV serialization.\n\nAfter applying a review fix:\n\n```\n/test-feature 10-export-expenses-csv/code-review-feature 10-export-expenses-csv\n```\n\nDo not ship from a stale pre-fix review.\n\n7. Ship\n\n```\n/ship-feature\n```\n\nThe captured run produced PR #3, squash-merged it, and removed both feature branches.\n\n/create-spec 10 export expenses csv checked the tree was clean, branched to feature/export-expenses-csv, delegated research to the Explore subagent, and wrote .claude/specs/10-export-expenses-csv.md with a fixed shape: Overview, Depends on, Routes, Database changes, Templates, Files to change, Files to create, New dependencies, Rules for implementation, Definition of done.\n\nThe implementation subagent read the spec, then CLAUDE.md, queries.py, app.py, profile.html, profile.css — and then grepped style.css for --token: declarations:\n\nThat grep is the spendly-ui-designer skill's rule *\"use the tokens, never hardcode a hex\"* being obeyed without anyone restating it. This is what a correctly wired setup looks like from the outside.\n\n/test-feature 10-export-expenses-csv ran writer → runner, sequentially:\n\n**25 test functions, 29 cases, each traceable to a Definition-of-Done item:**\n\n/code-review-feature 10-export-expenses-csv forked both reviewers in parallel:\n\nVisible in the dashboard as two Agent spawns from one Main:\n\nThe finding that justified the whole pipeline: security cleared the auth guard, SQL-level user_id scoping, and the parameterised date filter — then flagged CSV formula injection via free-text description and category. A cell beginning =, +, -, or @ executes as a formula when the file opens in Excel. Fixed in the same PR with a _csv_safe() helper. Severity: Medium. Caught pre-merge.\n\nShip. /ship-feature → PR #3, squash-merged, both branches deleted:\n\nWith the audit trail written into the PR body:\n\nIn this project, that behavior is **explicitly instructed in the test-writer agent itself**.\n\nThe main source is:\n\n```\n.claude/agents/spendly-test-writer.md\n```\n\nIts description says the agent generates tests from the feature’s **expected behavior and spec, not by reading the implementation code**\n\nResult: feature live, 186 → 215 tests.\n\nThe request arrived as one sentence or from a new specification: *“change the currency from Rs to BDT, change the default name, make it feel Bangladeshi, and change the design.”*\n\nThis is a good example of why the spec can decide not to generalize a change.\n\nStart with:\n\n```\n/create-spec 11 bangladesh localization\n```\n\nThe real .claude/specs/11-bangladesh-localization.md explicitly records two decisions before implementation:\n\n```\nDecision 1:Keep Western \"{:,.2f}\" digit grouping.Do not introduce lakh/crore formatting.\nDecision 2:Keep demo@spendly.com / demo123.Change display/context data, not the documented demo login.\n```\n\nThe spec also says:\n\n**That last point is subtle and important:**\n\nhistorical specs are records of their time; the current CLAUDE.md becomes the forward-looking source of truth.\n\nPlan prompt\n\n```\nRead .claude/specs/11-bangladesh-localization.md and CLAUDE.md.\nPlan a content/configuration-only change. Identify every exact literal that changesand every file that must remain untouched. Pay special attention to:- database/queries.py must have zero diff,- demo login credentials must not change,- historical specs 05-09 stay unchanged,- the UI skill and seed commands must not retain Indian-context instructions.\nDo not edit yet.\n```\n\nImplementation prompt\n\n```\nImplement the approved Step 11 localization plan exactly.Make only the literal/content/configuration changes named in the spec.Do not refactor number formatting or database queries.\n```\n\nTesting is intentionally different in this case\n\nCurrent main has no tests/test_11_bangladesh_localization.py. The spec itself requires updating the existing tests/test_06_date_filter_profile.py currency assertions and then running the full suite plus verify_setup.py.\n\nSo the honest validation sequence for this historical case is:\n\n```\npython -m pytest tests/test_06_date_filter_profile.py -vpython -m pytest -qpython .claude/verify_setup.pygit diff -- database/queries.pygit diff -- .claude/specs/\n```\n\nThis is an important exception to avoid papering over: the current generic /test-feature command expects a feature-specific test file, while Step 11's final repository state used existing coverage. A reusable team workflow should either add a dedicated Step-11 test file or teach /test-feature to honor a spec's explicit \"modify existing tests\" strategy.\n\nReview and ship\n\n```\n/code-review-feature 11-bangladesh-localization\n```\n\nCheck especially for scope creep and stale instruction files. After an approved review and a green full suite:\n\n```\n/ship-feature\n```\n\nStart with:\n\n```\n/create-spec 12 bangladesh design refresh\n```\n\nThe real spec demonstrates a design decision captured inside the specification. It proposed three palettes, then recorded:\n\n```\nDecision: Option A--accent:         #006A4E--accent-light:   #E3F3EC--accent-2:       #F42A41--accent-2-light: #FDE6E8--paper / --ink:  unchanged\n```\n\nThe spec then deliberately narrowed the implementation:\n\nPlan prompt\n\n```\nRead .claude/specs/12-bangladesh-design-refresh.md and thespendly-ui-designer skill.\nPlan Option A exactly as selected by the spec.Keep the change token-driven: do not rewrite landing.css, profile.css oranalytics.css. Include the skill-documentation update and visual verificationsteps. Do not edit yet.\n```\n\nImplementation prompt\n\n```\nImplement the approved Step 12 plan.Use only the chosen Option A token values.Do not introduce new palette choices or layout changes.Update the UI skill in the same change so the reviewer reads the new palette asground truth.\n```\n\nTest\n\n```\n/test-feature 12-bangladesh-design-refresh\n```\n\nThe current repository contains tests/test_12_bangladesh_design_refresh.py, so this case follows the dedicated feature-test pipeline cleanly.\n\nThen perform the spec’s visual checks:\n\n```\n/              before + after/profile       before + after/expenses/add  before + after<768px         responsive verification\n```\n\nReview\n\n```\n/code-review-feature 12-bangladesh-design-refresh\n```\n\nThe quality reviewer reading the UI skill is part of the design: if the skill were left stale, correct CSS could be reported as a violation.\n\nShip\n\n```\n/ship-feature\n```\n\nThe final repository test file and current palette confirm this became executable, reviewable project state rather than a one-off styling prompt.\n\nThat is two features, and splitting them was the highest-value decision.\n\nMixed into one PR we cannot tell a broken layout from a broken currency render, and if the design is wrong we lose the localization on rollback.\n\nStep 11 — every ₹ → ৳, placeholders to Faizul islam/ faizul@example.com, and seed data rewritten to Bangladeshi context: Groceries from Meena Bazar, CNG fare to office, DESCO electricity bill.\n\nTwo decisions were settled *in the spec*, before any code:\n\nStep 12 — --accent: #1a472a → #006A4E (Bangladesh bottle green), --accent-2: #c17f24 → #F42A41 (flag red), new favicon.svg, +37 tests.\n\nThe critical instruction: update spendly-ui-designer/SKILL.md in the same change. It hardcodes the palette, and the quality reviewer reads it as ground truth — leave it stale and the reviewer flags our *correct* new code as a violation. We can watch it happen:\n\nNot a feature: a prerequisite. The app as written could not run outside a dev checkout, for four independent reasons.\n\nWhy Phase 0 is not run through /create-spec\n\nThis is the useful counter-example to the four feature cases above. Phase 0 is a deployment prerequisite owned by the DevOps command/agent/skill chain, so forcing it through the normal feature-spec command would duplicate the deployment knowledge already encoded in spendly-devops.\n\nThe workflow is:\n\n```\n/deploy-phase 0   ↓spendly-devops-engineer   ↓Skill(spendly-devops)   ↓phase-0 rules/invariants   ↓implementation artifacts/code changes   ↓spendly-devops-reviewer   ↓handover\n```\n\nA good user prompt is simply:\n\n```\n/deploy-phase 0\n```\n\nor, when coming from a natural-language request:\n\n```\nPrepare Spendly for deployment. Start with Phase 0 only.Do not move to Docker/cloud yet.\n```\n\nThe DevOps router/skill is responsible for recognizing that the application first needs secret handling, persistent state, safe seeding, and health/readiness semantics.\n\nAfter implementation, verify the application tests and wiring checks, then use the deployment reviewer. Only after Phase 0 is green should /deploy-phase 1 create the Docker artifacts.\n\nThis distinction is deliberate:\n\n```\nproduct feature   → /create-spec → Plan → build → /test-feature → /code-review-feature → /ship-featuredeployment prerequisite   → /deploy-phase → engineer → DevOps skill → DevOps reviewer → controlled ship\n```\n\nRun via /deploy-phase 0. Its pre-flight:\n\nThen engineer → reviewer:\n\nA design detail worth stealing: /healthz and /readyz are deliberately different.\n\n``` python\n@app.route(\"/healthz\")def healthz():    \"\"\"Liveness — the process is up. Deliberately does NOT touch the DB.\"\"\"    return {\"status\": \"ok\"}, 200@app.route(\"/readyz\")def readyz():    \"\"\"Readiness — the DB is reachable and writable.\"\"\"    if not db_is_healthy():        abort(503)    return {\"status\": \"ready\"}, 200\n```\n\nA liveness probe that touches the database turns a *locked* database into a *restart loop*, which makes the lock worse. Liveness asks “is the process wedged”; readiness asks “should traffic come here”. Conflating them is a classic self-inflicted outage.\n\nA repo-specific trap this surfaced: verify_setup.py hardcodes the route list twice — forward (\"claimed routes exist in app.py\") and reverse (\"no undocumented routes\"). Adding two routes fails *both* until CLAUDE.md's table and both literals are updated. The duplication is intentional; a comment now says so, so nobody \"fixes\" the failure by deleting a list.\n\nOur project contains many Claude-related components:\n\n```\nCLAUDE.md.claude/├── agents/├── skills/├── commands/├── hooks/└── settings.jsonspecs/tests/application files\n```\n\nThese components reference one another. For example:\n\n```\nSlash command     ↓references an Agent     ↓Agent may use project conventions     ↓Skill provides reusable knowledge\n```\n\nverify_setup.py acts as a **structural/integration sanity checker** for this configuration.\n\nFor example, imagine a command says:\n\n```\nUse the spendly-test-writer agent\n```\n\nbut somebody later renames:\n\n```\nspendly-test-writer.md\n```\n\nto:\n\n```\ntest-writer.md\n```\n\nNow our Claude workflow has a broken reference.\n\nThe application itself may still work perfectly:\n\n```\npytest252 passed\n```\n\nbut Claude automation could be broken.\n\nverify_setup.py is intended to catch this kind of problem.\n\nSpec-Driven Development (SDD) is easiest to understand as a contract-first control loop. The specification defines observable behaviour before implementation. Code, tests, and review are then evaluated against that contract.\n\nIt is related to, but not identical with, several familiar practices:\n\nSDD can include TDD or BDD. The important difference is that the specification is an explicit first-class artifact and review gate, rather than an assumption that lives only in a prompt or developer’s head.\n\nA strong feature spec usually separates:\n\nThis prevents the common failure where a spec is merely an implementation plan. A plan says *how we think we will build it*; a spec says *what must be true when we are done*. The plan may change while the contract remains stable.\n\nIf the test writer studies implementation details first, it can accidentally mirror bugs. This is called implementation-coupled testing: the test asks whether the code behaves like itself rather than whether it behaves like the requirement.\n\nA cleaner chain is:\n\n```\nSpecification   ├──→ implementation   └──→ independent tests             ↓       compare at runtime\n```\n\nBoth code and tests derive from the same contract but are produced independently. That creates useful disagreement. If they conflict, the spec becomes the arbitration point.\n\nThe DoD-to-test table shown in the Spendly run is a lightweight requirements traceability matrix. In regulated systems this idea is formal; here it is pragmatic: every important acceptance statement should have an observable validation path.\n\nA simple structure is enough:\n\n```\nDoD item → test(s) → result → reviewer finding → PR evidence\n```\n\nThis provides three benefits:\n\nThe two review points before implementation are where humans have the highest leverage. Fixing a misunderstood requirement in a 30-line spec is cheaper than fixing it after code, tests, documentation, and deployment artifacts have all been created from the wrong assumption.\n\nThat is the economic reason for SDD: move disagreement earlier, when change is cheap.\n\nFour, cycled with Shift+Tab:\n\nSet a default in settings.json:\n\n```\n{ \"permissions\": { \"defaultMode\": \"acceptEdits\" } }\n```\n\nOr launch straight into bypass:\n\n```\nclaude --dangerously-skip-permissionsclaude -c --dangerously-skip-permissions      # -c resumes the current conversation\n```\n\nIf ~/.claude/settings.json has \"skipDangerousModePermissionPrompt\": true, the startup warning is pre-dismissed.\n\nThis is the most under-used mode. In plan mode the model cannot write, which changes its behaviour:\n\nit reads more, and it surfaces disagreements *before* they are baked into a diff. Our CLAUDE.md makes it mandatory:\n\n```\n- always use a builtin plan subagent in plan mode- When asked to plan, delegate codebase research to a subagent before presenting\n```\n\nEnter with Shift+Tab twice. Exit by approving the plan.\n\nSeparate from permissions, Claude Code has reasoning effort: low, medium, high, xhigh, max. The status line shows the active tier:\n\n```\nSonnet 5 (1M) | medium (default, active: default) | main | $0.3781.2k tokens | 8% | 39% (resets 4h 15m)bypass permissions on (shift+tab to cycle)\n```\n\nEscalate for hard reasoning — an ambiguous bug, an architectural trade-off, an adversarial review. Drop to low for mechanical work. Most of Spendly ran on medium.\n\nThere is also a genuinely separate feature:\n\n```\nclaude ultrareview            # cloud-hosted multi-agent review of the current branch\n```\n\nThat runs a *fleet* rather than a single reviewer, which is a different tool from raising effort on one agent.\n\n--dangerously-skip-permissions silences the harness. It does not silence the model's judgment — Claude will still pause before hard-to-reverse or outward-facing actions. In this project, even in bypass mode, it stopped to ask before untracking database files containing real password hashes.\n\nTo collapse that second layer we must say so in words: *“commit and push without asking”*, *“merge PRs yourself”*. Two layers, two mechanisms.\n\nA common conceptual mistake is to treat “YOLO” as if it makes the model more intelligent or “plan mode” as if it makes the model more cautious by personality. These settings operate on different axes.\n\nPermission mode controls what the harness will allow without human approval. It is an execution control.\n\nReasoning effort controls how much deliberate reasoning the model spends. It is a cognition/compute control.\n\nThis creates a 2D decision space:\n\n```\n                 more reasoning                      ↑     plan/high        │       bypass/high   safe deep design   │   autonomous deep work                      │ ─────────────────────┼────────────────────→ more execution autonomy                      │ default/low          │       bypass/low quick inspection     │   mechanical automation\n```\n\nThe right combination depends on risk, reversibility, and uncertainty.\n\nBefore choosing a mode, ask:\n\nHigh blast radius + low reversibility is a strong argument for default/plan mode, even when the code change itself looks simple.\n\nThe guide correctly distinguishes harness permission from model judgment. There is also a third layer worth naming: organizational controls outside Claude, such as branch protection, required reviews, cloud IAM, CI gates, and production approval workflows.\n\n```\nLayer 1: Claude permission promptsLayer 2: model instructions / judgment / hooksLayer 3: external systems that enforce policy regardless of Claude\n```\n\nFor important systems, rely on layer 3 for true enforcement. A local bypass flag should never be able to bypass an organization’s production guardrail.\n\nThe safest place for bypass mode is an environment where the maximum possible damage is already constrained: a disposable branch, container, test account, ephemeral VM, or isolated development namespace. That is the security principle of containment: instead of trusting every future action to be correct, design the environment so an incorrect action has limited consequences.\n\nThe distinction that took us longest to internalise:\n\nA command decideswhat happens in what order. An agent decideswho does it, with which tools. A skill supplieswhat they need to know.\n\nNote the difference between → and ∥. Test-writer must finish before the runner has a file to run. The two reviewers are independent, so they fork — visible as two Agent spawns in the dashboard.\n\nThe chain only works because the agent definition says so, as its *first instruction*:\n\n```\n## Step 1 — Load the skill. Always. Before anything else.1. Invoke the `spendly-devops` skill using the **Skill** tool.2. If that fails, read `.claude/skills/spendly-devops/SKILL.md` with **Read**.** Do not write a single artifact from general Docker or Kubernetes knowledge.**The skill carries traps specific to this repo that generic knowledge will miss.\n```\n\nBelt and braces — the Skill tool *and* a Read fallback, so the chain does not depend on one mechanism.We can watch it work:\n\nThat Skill spendly-devops line is the policy executing.\n\n```\n# spendly-devops-engineertools: Read, Write, Edit, Grep, Glob, Bash, Skill\n# spendly-devops-reviewertools: Read, Grep, Glob, Bash(git diff), Bash(git status), Skill\n```\n\nThe reviewer structurally cannot edit a file. Not “is told not to” — cannot. That is a much stronger guarantee than instruction-following, and it is free.\n\nEvery link above is a string in a markdown file. Rename an agent and the command still names the old one — and it fails *silently at runtime*, because a missing subagent looks like a subagent that had nothing to say.\n\nWe hit exactly this. So:\n\n```\npython .claude/verify_setup.py     # 56 checks; non-zero exit on any break\n```\n\nIt verifies:\n\nIt found real breaks every time the setup changed. If you build a chain like this, build the verifier too — it is 150 lines and it is the difference between wiring you trust and wiring you hope about.\n\nThe command → agent → skill architecture maps cleanly to three software-design ideas.\n\nCommand = orchestration. It owns workflow state and ordering.\n\nAgent = execution boundary. It owns context isolation, role, and capabilities.\n\nSkill = reusable domain knowledge. It owns procedure, heuristics, traps, and reference material.\n\nThe reason to keep them separate is the same reason application code separates controllers, services, and libraries: each changes for a different reason.\n\nIf one large prompt contains all three concerns, every change risks unintended side-effects elsewhere.\n\nA robust chain has explicit contracts:\n\n```\nCommand → Agent input:  objective, scope, constraints, expected output\nAgent → Skill input:  domain/task classification\nSkill → Agent output:  rules, references, validation procedure\nAgent → Command output:  handover block / findings / status\n```\n\nThe ## Handover convention in the guide is an example of a simple interface. It makes outputs machine- and human-predictable.\n\nUse sequential execution when there is a data dependency:\n\n```\ntest writer → test runner\n```\n\nThe runner cannot validate tests that do not yet exist.\n\nUse parallel execution when tasks are independent and you want diversity:\n\n```\nsecurity reviewer ─┐                   ├→ combined reviewquality reviewer ──┘\n```\n\nParallel reviewers are useful not only for speed but for independent failure modes. A security reviewer and a quality reviewer are primed to notice different classes of defects.\n\nBecause the system has no compiler for its Markdown references, the verifier is a form of contract test for the agent architecture. The generalizable pattern is:\n\nIf a configuration error would otherwise fail only when a rare workflow is invoked, move that failure to a cheap automated check that runs frequently.\n\nThis is exactly what unit tests, schema validation, CI linting, and Terraform validation already do in other domains.\n\nBeyond custom agents, Claude Code ships general-purpose ones. The two we lean on:\n\n/create-spec delegates research to Explore rather than reading files itself:\n\n```\n## Step 6 — Research the codebase (delegate this)`CLAUDE.md`'s Subagent Policy requires codebase research to be delegated. Do notread these files yourself — launch the builtin **`Explore`** subagent with breadth\"medium\" and ask it to report:  - app.py — every existing route, its methods, and its auth guard  - database/db.py and database/queries.py — the DB layer is two modules  - .claude/specs/*.md — so the new spec does not duplicate an existing one\n```\n\nWhy delegate at all?\n\nContext economy. Explore burns *its own* window reading twelve specs and two DB modules, then hands back a summary. The main session pays for the conclusion, not the search. On a repo this size that is the difference between having room to implement and running out mid-feature.\n\nOur policy, verbatim:\n\n```\n## Subagent Policy- Always use a builtin explore subagent for codebase exploration before  implementing any new feature- Always use a subagent to verify test results after any implementation- When asked to plan, delegate codebase research to a subagent before presenting- always use a builtin plan subagent in plan mode\n```\n\nSubagents are most valuable when they reduce one of three pressures on the main session:\n\nThey are less useful when the task is tiny or when every agent must repeatedly read the same large context. Delegation has overhead: spawning, rediscovering context, communicating results, and reconciling disagreement.\n\nA useful threshold is:\n\n```\nIf explaining the task to a subagent costs nearly as much as doing the task,keep it in the main session.\n```\n\nA reviewer in a fresh context is less anchored to the implementation story told by the authoring agent. This is analogous to independent code review: the reviewer may notice an authorization flaw precisely because it did not participate in the design choices that made the code look “obviously correct” to the implementer.\n\nThat does not make subagents objectively independent — the same model family may be used — but separate prompts and context still reduce shared local assumptions.\n\nConcurrent agents should usually operate on either:\n\nTwo writing agents in one working tree can overwrite each other’s assumptions or observe half-written files. The failure described later in the guide is a classic shared-state race condition. Agent orchestration therefore benefits from ordinary concurrency principles: ownership, isolation, synchronization, and clear handoff.\n\nWe do not use MCP in Spendly. This section is theory plus a working design, and an honest account of *why* we did not need it.\n\nThe Model Context Protocol is an open standard that lets a model call tools hosted in a separate process. Instead of teaching the model to shell out to psql, you run an MCP server that exposes query_database as a typed tool with a schema.\n\n```\n┌──────────────────────────────────────────────────────────┐│                     Claude Code                          ││                                                          ││   ┌────────────────┐        ┌──────────────────────┐     ││   │ Built-in tools │        │   MCP client         │     ││   │ Read/Edit/Bash │        │                      │     ││   └────────────────┘        └──────────┬───────────┘     │└────────────────────────────────────────┼─────────────────┘                                         │  JSON-RPC 2.0                        ┌────────────────┼────────────────┐                        │ stdio          │ HTTP/SSE       │              ┌─────────▼──────┐  ┌──────▼───────┐  ┌─────▼────────┐              │ spendly-db     │  │ github       │  │ docker       │              │ MCP server     │  │ MCP server   │  │ MCP server   │              │                │  │              │  │              │              │ tools:         │  │ tools:       │  │ tools:       │              │  query_expenses│  │  create_pr   │  │  ps, logs    │              │  schema_info   │  │  merge_pr    │  │  compose_up  │              │  row_counts    │  │  list_issues │  │  inspect     │              └────────┬───────┘  └──────┬───────┘  └─────┬────────┘                       │                 │                │                  ┌────▼─────┐      ┌────▼────┐     ┌─────▼──────┐                  │ SQLite   │      │ GitHub  │     │ Docker     │                  │ spendly. │      │ REST v3 │     │ daemon     │                  │ db       │      │         │     │ socket     │                  └──────────┘      └─────────┘     └────────────┘\n```\n\nTransports: stdio (local subprocess — most common) or HTTP/SSE (remote). Primitives: *tools* (model-invoked actions), *resources* (readable data), *prompts* (reusable templates).\n\nMCP earns its complexity when a capability is not reachable from a shell or when you want a typed, constrained interface instead of arbitrary commands.\n\nEvery capability was one shell command away. Adding MCP would have meant a second process, a schema to maintain, and tool definitions in the context window — for nothing. The honest engineering answer was: not here.\n\nConcretely, for a Python web project with DB + git + Docker:\n\nHere is the read-only server that *would* have been defensible — Bash minus the ability to write:\n\n```\n# mcp_servers/spendly_db.py\"\"\"Read-only MCP server over Spendly's SQLite database.\nDeliberately narrower than Bash(sqlite3 *): SELECT only, capped rows, noattach/pragma, and the DB path comes from the environment rather than the caller.\nRun:  python mcp_servers/spendly_db.pyRegister in .mcp.json (see below).\"\"\"\npython\nimport osimport reimport sqlite3from pathlib import Path\npython\nfrom mcp.server.fastmcp import FastMCP\nmcp = FastMCP(\"spendly-db\")\nDB_PATH = os.environ.get(\"SPENDLY_DB_PATH\", \"spendly.db\")MAX_ROWS = 500\nphp\n# One statement, starting with SELECT. No semicolons -> no statement chaining._SELECT_ONLY = re.compile(r\"^\\s*SELECT\\b\", re.IGNORECASE)_FORBIDDEN = re.compile(    r\"\\b(ATTACH|DETACH|PRAGMA|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|REPLACE)\\b\",    re.IGNORECASE,)\nphp\ndef _connect() -> sqlite3.Connection:    # Immutable URI: the driver itself refuses writes, so a bug in our regex    # cannot become a mutation. Defence in depth, not just validation.    conn = sqlite3.connect(f\"file:{Path(DB_PATH)}?immutable=1\", uri=True)    conn.row_factory = sqlite3.Row    return conn\nphp\n@mcp.tool()def query_readonly(sql: str) -> dict:    \"\"\"Run a single read-only SELECT and return rows as JSON.\nRejects anything that is not one SELECT statement. Caps output at 500 rows.    \"\"\"    if \";\" in sql.rstrip().rstrip(\";\"):        return {\"error\": \"one statement only — no semicolons\"}    if not _SELECT_ONLY.match(sql):        return {\"error\": \"only SELECT is permitted\"}    if _FORBIDDEN.search(sql):        return {\"error\": \"statement contains a forbidden keyword\"}\nconn = _connect()    try:        rows = conn.execute(sql).fetchmany(MAX_ROWS)        return {            \"columns\": [d[0] for d in conn.execute(sql).description],            \"rows\": [dict(r) for r in rows],            \"row_count\": len(rows),            \"truncated\": len(rows) == MAX_ROWS,        }    except sqlite3.Error as exc:        return {\"error\": f\"{type(exc).__name__}: {exc}\"}    finally:        conn.close()\nphp\n@mcp.tool()def schema_info() -> dict:    \"\"\"Return every table and its columns — orientation without guessing.\"\"\"    conn = _connect()    try:        tables = [            r[\"name\"]            for r in conn.execute(                \"SELECT name FROM sqlite_master WHERE type='table' \"                \"AND name NOT LIKE 'sqlite_%' ORDER BY name\"            )        ]        return {            t: [                {\"name\": c[\"name\"], \"type\": c[\"type\"], \"notnull\": bool(c[\"notnull\"])}                for c in conn.execute(f\"PRAGMA table_info({t})\")            ]            for t in tables        }    finally:        conn.close()\nphp\n@mcp.tool()def row_counts() -> dict:    \"\"\"Row count per table — the cheapest sanity check after a migration.\"\"\"    conn = _connect()    try:        tables = [            r[\"name\"]            for r in conn.execute(                \"SELECT name FROM sqlite_master WHERE type='table' \"                \"AND name NOT LIKE 'sqlite_%'\"            )        ]        return {t: conn.execute(f\"SELECT COUNT(*) FROM {t}\").fetchone()[0] for t in tables}    finally:        conn.close()\nphp\n@mcp.resource(\"spendly://schema\")def schema_resource() -> str:    \"\"\"The schema as a readable resource, for context rather than a tool call.\"\"\"    return \"\\n\".join(        f\"{t}: {', '.join(c['name'] for c in cols)}\"        for t, cols in schema_info().items()    )\nif __name__ == \"__main__\":    mcp.run()          # stdio transport\n```\n\nRegister it — project-scoped, in .mcp.json at the repo root:\n\n```\n{  \"mcpServers\": {    \"spendly-db\": {      \"command\": \"python\",      \"args\": [\"mcp_servers/spendly_db.py\"],      \"env\": { \"SPENDLY_DB_PATH\": \"spendly.db\" }    }  }}\n```\n\nThen /mcp lists it, and tools appear as mcp__spendly-db__query_readonly.\n\nNote the layered defence: a regex allowlist *and* immutable=1 on the connection. If the regex has a hole, the driver still refuses to write. Validation alone is how MCP servers become the vulnerability they were meant to prevent.\n\nMCP servers add context. Tool schemas are injected. Prefer few, well-scoped tools over dozens.\n\nThey can vanish. Mid-project our Docker MCP server disconnected and its tools became uncallable. Anything load-bearing needs a fallback — which is precisely why our agents specify the Skill tool *and* a Read fallback.\n\nA clever pattern we found in the wild: agents-observe ships an MCP server that exposes zero tools. It is a *lifecycle hook* — it starts a Docker container, heartbeats every 10s, and deregisters on SIGTERM. Zero context cost, real work done. MCP as a process supervisor.\n\n```\nClaude starts    │    ▼MCP server starts    │    ├── Starts a Docker container    ├── Sends heartbeat every 10 seconds    ├── Keeps running in background    │    ▼Claude/session terminates    │    ▼MCP receives SIGTERM    │    ├── Deregisters itself    └── Cleans up\n```\n\nThe MCP server doesn’t give Claude callable functions such as:\n\n```\nget_metrics()restart_container()query_logs()\n```\n\nThere are **no MCP tools for Claude to choose from**.\n\nInstead, simply **starting the MCP server causes useful background work to happen**.\n\nThat’s why we call it a:\n\nlifecycle hook\n\nThe MCP process’s **start and stop lifecycle** is being used as the trigger.\n\nNormally, if an MCP server exposes 20 tools, Claude needs information about those tools so it understands when/how to use them.\n\nConceptually:\n\n```\n20 MCP tools     ↓tool names + descriptions + schemas     ↓added information Claude must understand\n```\n\nWith this pattern:\n\n```\nMCP tools = 0      ↓No tool definitions for Claude      ↓Very little/no tool-schema context overhead\n```\n\nYet the MCP process itself can still do work:\n\n```\nstart containermonitor lifecyclesend heartbeatregister agentclean up\n```\n\nSo the phrase means:\n\nClaude doesn’t need to spend context understanding/calling MCP tools, but the MCP server still performs useful background work.\n\nThis is the most important sentence.\n\nUsually:\n\n```\nMCP = Claude ↔ external tools/data\nMCP = start → supervise → stop background process\n```\n\nFor example:\n\n```\nClaude Code session       │       ▼ agents-observe MCP       │       ├── start Docker container       │       ├── keep it alive       │       ├── heartbeat       │       └── watch lifecycle       │Claude exits       ▼    SIGTERM       │       ▼ deregister / cleanup\n```\n\nSo **“process supervisor”** means it manages the lifetime of another running process/container.\n\nThis is different from the observability MCP design we might normally imagine.\n\nA conventional approach could be:\n\n```\nClaude   │   └── MCP        ├── query Prometheus        ├── query Loki        └── query Grafana\n```\n\nClaude explicitly calls tools.\n\nThe pattern described in our quote is closer to:\n\nSo the clever idea is **not “MCP lets Claude call something.”**\n\nIt is:\n\n“Because Claude automatically manages the MCP server’s lifecycle, we can exploit that lifecycle to automatically start and stop another service — even though Claude never calls an MCP tool.”\n\nThat’s why we describes it as **MCP as a process supervisor**.\n\nMCP is often introduced as “a way to connect Claude to external tools.” That is true, but the more important architectural idea is capability shaping.\n\nWith unrestricted shell access, the model receives a broad capability:\n\n```\nBash → potentially every executable and every permission of the current user\n```\n\nAn MCP server can replace that with a narrow capability:\n\n```\nquery_readonly(sql: SELECT only, max 500 rows)\n```\n\nThis is a security improvement when the MCP implementation is actually narrower than the shell access it replaces.\n\nThe model does not directly “know” how to talk to SQLite, Jira, or an internal service. Claude Code acts as an MCP client. A server publishes typed capabilities, and the client exposes those descriptions to the model.\n\n```\nModel reasoning   ↓ chooses toolClaude Code / MCP client   ↓ protocol requestMCP server   ↓ validates + actsTarget system\n```\n\nThe server is therefore a trust boundary. It must validate arguments, enforce authorization, limit output, handle credentials safely, and return errors that do not leak secrets.\n\nThe distinction matters for least privilege. If the model only needs to read schema information, a resource is conceptually safer than a generic query tool.\n\nJSON schemas and structured return values help the model call a capability correctly, but typing is not authorization. A perfectly typed delete_database(name) tool is still dangerous. Security must be enforced inside the server and in the credentials it holds.\n\nThe example combines two independent controls:\n\nThis is stronger because a bug in one layer does not immediately become a write. The general principle is:\n\nPrefer a safe underlying primitive plus validation, rather than relying on a parser/regex alone to make a dangerous primitive safe.\n\nMCP adds operational surface area: process lifecycle, authentication, versioning, logging, schema maintenance, context cost, and failure handling. If git status or docker ps already solves the problem safely under a restricted command allowlist, an MCP wrapper may be unnecessary abstraction.\n\nUse MCP when it creates one of these concrete benefits:\n\nA skill is *advice*. A hook is *mechanism*. Skills persuade the model;\n\nhooks execute regardless of what the model concludes.\n\nThis distinction has a sharp consequence:\n\nHooks still fire under--dangerously-skip-permissions.\n\nA PreToolUse hook returning exit code 2 blocks the tool call in any permission mode. So even in full YOLO, rm -f spendly.db is stopped. You build a floor that YOLO cannot fall through.\n\n.claude/settings.json:\n\n```\n{  \"hooks\": {    \"UserPromptSubmit\": [      { \"hooks\": [{ \"type\": \"command\", \"command\": \"python3 .claude/hooks/devops_router.py\" }] }    ],    \"PostToolUse\": [      { \"matcher\": \"Write|Edit\",        \"hooks\": [{ \"type\": \"command\", \"command\": \"python3 .claude/hooks/format_python.py\" }] }    ],    \"PreToolUse\": [      { \"matcher\": \"Bash\",        \"hooks\": [{ \"type\": \"command\", \"command\": \"python3 .claude/hooks/protect_paths.py\" }] }    ]  }}\n```\n\nContract: JSON payload on stdin. For UserPromptSubmit, anything on stdout is injected into the model's context. For PreToolUse, exit 2 denies the call and stderr is fed back to the model.\n\nThere are ~28 events available — SessionStart, SubagentStart, SubagentStop, PreCompact, PermissionDenied, TaskCompleted, FileChanged, and more.\n\n```\nPROTECTED = [\"spendly.db\", \"spendly-backup.db\", \".env\", \"migrations/\",             \"/var/lib/spendly\", \"letsencrypt\"]DESTRUCTIVE_VERBS = [r\"rm\", r\"unlink\", r\"truncate\", r\"shred\", r\"mkfs(\\.\\w+)?\", r\"dd\"]\n```\n\nDestructive verb and protected path → exit 2, blocked\n\n2. format_python.py — cosmetic\n\nRuns black on any .py file written. No-ops with a message if black is not installed.\n\n3. devops_router.py — the interesting one\n\nThe problem it solves: a new teammate types *“can you dockerize this?”*. They do not know /deploy-phase exists. Without help, the session writes a plausible Dockerfile from general knowledge — and bakes the SQLite database into an image layer, because it does not know DB_PATH is hardcoded.\n\nThe hook matches DevOps vocabulary and injects routing instructions before the model sees the prompt:\n\n```\n<devops-routing source=\".claude/hooks/devops_router.py\">This prompt matched Spendly DevOps triggers (docker). Handle it through theDevOps pipeline rather than ad hoc:1. Load the `spendly-devops` skill BEFORE producing any artifact.2. Delegate to `spendly-devops-engineer` via the Agent tool.3. Phase 0 is a hard prerequisite for phases 1-3.4. Relay the subagent's `## Handover` block, then stop.5. Never commit, push, or touch live cloud state without approval.</devops-routing>\n```\n\nTwo-tier matching keeps it precise, which matters in a Flask repo full of DevOps-adjacent words:\n\n```\nSTRONG = [r\"docker\", r\"kubernetes\", r\"kubectl\", r\"pods?\\b\", r\"ec2\\b\", r\"nginx\", ...]WEAK   = [r\"server\", r\"image\", r\"container\", r\"volume\", r\"restore\", r\"cloud\", ...]\n```\n\nOne STRONG hit routes. WEAK needs two distinct hits. Measured: 11/11 fire, 10/10 stay quiet.\n\nprotect_paths.py shipped with five false positives, each blocking an ordinary command:\n\nThat last one blocked every commit carrying a Co-Authored-By trailer. The docstring even claimed word boundaries prevented the confirm case — they did not, because only the *trailing* boundary was present.\n\nSo tests/test_hooks.py — 48 tests driving each hook over its real stdin/stdout JSON contract, pinning every false positive:\n\n```\n@pytest.mark.parametrize(\"command,why\", [    (\"git add -A\", \"'add' contains 'dd' but is not the dd command\"),    (\"echo 'confirm spendly.db'\", \"'confirm' contains 'rm' but is not rm\"),    (\"git rm --cached spendly.db\", \"index-only, file stays on disk\"),    (\"ls spendly.db >/dev/null 2>&1\", \"discarding output destroys nothing\"),])def test_safe_commands_are_allowed(self, command, why):    code, _, _ = run_hook(\"protect_paths.py\", {\"tool_input\": {\"command\": command}})    assert code == ALLOW, f\"false positive ({why}): {command}\"\n```\n\nA structural limitation, stated honestly: the guard matches substrings and has no shell parser. It cannot distinguish a command from a string mentioning one — it blocked our own test harness for containing rm spendly.db as *data*. That is why hook test cases live in a file rather than inline in a shell command. A guardrail is protection against accidents, not a security boundary.\n\nWe nearly “fixed” cross-platform support like this:\n\n```\n\"command\": \"python3 hook.py || python hook.py\"\n```\n\nThat silently disables the guard. protect_paths.py signals a block with exit 2; || reads that as failure, reruns against already-consumed stdin, gets empty input, exits 0 — and the destructive command proceeds. The reason is now a comment in settings.json so nobody re-adds it.\n\nLanguage-model reasoning is probabilistic: even with excellent instructions, the model can interpret a situation differently on another run. Hooks exist to place deterministic checks at lifecycle boundaries.\n\nThat makes the basic split:\n\n```\nSkill / instruction:  \"Do not delete the production database.\"\nPreToolUse hook:  If command matches destructive operation + protected path → exit 2.\n```\n\nThe first shapes judgment. The second enforces a mechanical condition.\n\nA hook has four conceptual parts:\n\nDesigning these explicitly helps avoid “hook sprawl,” where many overlapping scripts run on every tool call with unclear ownership.\n\nA security control must decide what happens when the control itself crashes.\n\nThe Python-not-found incident later in our conversation is a perfect example of why this distinction matters: non-blocking hook failures allowed commands to continue, so the repository remained usable but the intended security check was not active.\n\nFor destructive production controls, fail-closed is often appropriate. For cosmetic formatting, fail-open is usually better.\n\nA useful hierarchy is:\n\n```\nExact rule / parser / scanner exists    → command hook\nJudgment is required but result is advisory    → prompt/model hook\nLarge cross-file reasoning required    → reviewer agent / skill\n```\n\nDo not ask an LLM to decide something a five-line deterministic check can prove. Conversely, do not force a fragile regex to solve a semantic authorization review.\n\nHooks sit in front of high-frequency actions, so false positives compound quickly. A 1% false-positive rate on a hook that runs hundreds of times per day becomes a constant source of interruption.\n\nThe Spendly tests demonstrate the right approach:\n\nThat turns guardrails from “clever regex” into maintained software.\n\n**Hooks are not the final security boundary**\n\nLocal hooks can be disabled, misconfigured, or bypassed by edits made outside Claude. Mandatory controls should also live in systems the local agent cannot override: branch protection, CI checks, IAM, admission policies, secret scanning, deployment approvals, and backups.\n\nA plugin bundles skills, commands, agents, hooks, and MCP servers into one installable unit, distributed via a marketplace (a git repo).\n\n```\nclaude plugin marketplace add <owner>/<repo>claude plugin install <name>claude plugin details <name>      # component inventory + projected token costclaude plugin listclaude plugin disable <name>      # keep installed, stop loadingclaude plugin uninstall <name>\n\"enabledPlugins\": {  \"aws-core@claude-plugins-official\": true,  \"aws-dev-toolkit@claude-plugins-official\": true,  \"databases-on-aws@claude-plugins-official\": true,  \"code-review@claude-plugins-official\": true,  \"code-simplifier@claude-plugins-official\": true,  \"security-guidance@claude-plugins-official\": true,  \"ralph-wiggum@claude-code-plugins\": true,  \"agents-observe@agents-observe\": true}\n```\n\naws-dev-toolkit earned its place immediately. Asked to deploy on \"t3.medium free tier\",\n\nwe queried its pricing MCP tool instead of reciting from memory:\n\nt3.medium is not free tier — Free Tier is 750 h/month of t2.micro (or t3.micro where t2 is unavailable), 12 months from account creation. That one lookup changed the deployment and saved ~$29/month.\n\nInstall: claude plugin details first. Ask what it adds *always-on* versus on-demand.\n\nTwo scope facts:\n\nA plugin is attractive because it packages many primitives behind one installation, but that convenience also creates a supply-chain boundary. A plugin may contribute:\n\nInstalling a plugin is therefore closer to installing a development tool than to copying a prompt from a blog post.\n\n1. Capability. What tools, processes, network access, or credentials does it need?\n\n2. Automatic execution. Which hooks run without explicit invocation, and on which events?\n\n3. Context cost. What descriptions/tool schemas are always present?\n\n4. Update trust. Who controls future versions, and how are changes reviewed?\n\nThis is why plugin details is valuable before installation: inventory first, execution second.\n\n**Marketplace versus plugin**\n\nA marketplace is a catalog and distribution source. A plugin is the executable/ instruction package. Keeping the two concepts separate makes organizational governance clearer:\n\n```\nCefalo marketplace   ├── approved internal plugin A   ├── approved internal plugin B   └── metadata / versions\nAnthropic official marketplace   └── official plugins selected by policy\n```\n\nA team can permit multiple marketplaces while still selecting only a small set of approved plugins.\n\n**Organization-level governance pattern**\n\nFor a professional team, a mature flow looks like:\n\n```\nDiscover → inspect → sandbox test → security review → approve version      → publish/allowlist → monitor updates → periodically re-review\n```\n\nHooks and MCP components deserve especially careful review because they can execute automatically or interact with external systems.\n\nTerminal output shows us *one* agent’s stream. Once a command forks two reviewers, scrollback stops being a useful mental model. agents-observe plugin renders the hierarchy in a browser.\n\n```\nclaude plugin marketplace add simple10/agents-observeclaude plugin install agents-observe# restart claude — hooks and the MCP server load at session start\n```\n\nThen /observe, or open [http://localhost:4981](http://localhost:4981/)\n\nPrerequisites: Docker (it runs a container) and Node.js (hook scripts), bash.\n\n```\nAlways-on:   ~27 tok       added to every sessionHooks (28)   harness-only — no model context costMCP (1)      tool schemas resolved at runtime; not countedobserve      ~30 always-on  /  ~1.9k on-invoke\n```\n\nTwenty-seven tokens. Our own spendly-devops description is 146. The real cost is latency, not context — its PreToolUse matcher is (all), so a bash→node chain spawns on *every* tool call, including every Read. On Windows, where process spawn is slow, that is noticeable.\n\n```\nports:  - \"${AGENTS_OBSERVE_BIND:-127.0.0.1}:${AGENTS_OBSERVE_SERVER_PORT:-4981}:...\"\n```\n\nLocalhost by default. AGENTS_OBSERVE_BIND=0.0.0.0 is documented for LAN access — and the dashboard has no authentication, so never set that on untrusted wifi. Verify after install:\n\n```\ndocker ps --format '{{.Names}} {{.Ports}}' | grep -i observe# want: 127.0.0.1:4981->4981/tcp\n```\n\nSequential handoff — writer finishes, SubStop, then Main spawns the runner:\n\nParallel fork — two Agent spawns from one Main:\n\nPolicy compliance — the engineer’s Skill spendly-devops call, visible:\n\nOnce one session spawns several agents, the workflow stops looking like a linear chat and starts looking like a small distributed system:\n\nThat is why a visual hierarchy becomes useful.\n\n**Execution observability answers what ran?**\n\n— which agents, tools, hooks, and commands were invoked.\n\n**Performance observability answers where did time/tokens go?**\n\n— long-running agents, tool-heavy branches, repeated searches.\n\n**Correctness observability answers did the workflow really complete?**\n\n— did a reviewer inspect files, return evidence, and satisfy the expected output contract?\n\nThe “Done with 0 tool uses” incident shows why status alone is insufficient. In reliable systems, completion should be inferred from evidence, not just a final state label.\n\nA better completion contract can include:\n\n```\nagent status = completedAND output is non-emptyAND required tool(s) were used where appropriateAND expected report section existsAND no API/tool error occurred\n```\n\nInstrumentation is never free. A hook that spawns Node on every tool call increases latency even if it contributes almost no model-context tokens. This mirrors ordinary APM systems: telemetry can consume CPU, network, storage, and time.\n\nMeasure both context overhead and runtime overhead before deciding a plugin is cheap.\n\nRather than one leap to production, four phases, each a superset:\n\nPhase 0 is not skippable. It is the four code changes from [Walkthrough 3](https://github.com/faizulkhan56/claude-setup-basic/blob/main/claude-code-properly-wired-full-consolidated-final.md#walkthrough-3-phase-0-making-the-app-deployable).\n\n```\n\"can you dockerize this?\"        ↓  devops_router.py  (deterministic, no model judgment)        ↓  CLAUDE.md DevOps policy  (always in context)        ↓  /deploy-phase 1        ↓  spendly-devops-engineer  →  spendly-devops-reviewer        ↓  Skill(spendly-devops) + Read(references/phase-1-docker.md)        ↓  ## Handover block relayed, then stop\n```\n\nOne rule made this safe:\n\n```\n4. **Never commit, push, or mutate live cloud or cluster state** as part of a   DevOps request. Print the command; let the user run it.\n```\n\nThe agent writes artifacts and prints AWS CLI commands. we run them. Every aws ec2 run-instances in this project was executed by a human who read it first:\n\nWhen the DevOps agent created the production Docker setup, it did not just create a Dockerfile. It made several engineering decisions based on how this specific Spendly application works, and it verified assumptions instead of blindly trusting documentation.\n\nKey decisions, each with a reason:\n\n1.gunicorn, not python app.py.\n\napp.py ends with app.run(debug=True, port=5001). debug=True serves the Werkzeug interactive debugger, which executes arbitrary Python from a browser. Reachable = compromised. The CMD imports app:app as a WSGI callable, so __main__ never runs and stays intact for local dev.so finally docker run\n\n```\ngunicorn app:appapp:app │   │ │   └── Flask object called \"app\" │ └────── Python module app.py\n```\n\n2.--workers 1 --threads 4.\n\nKeep one process touching SQLite, but use several threads so the web server can still handle concurrent requests.\n\nget_db() opens a fresh connection per call, so the code is thread-safe. It is *not* multi-process safe for writes — two gunicorn worker processes on one SQLite file produce database is locked. Threads give concurrency without cross-process write contention.\n\n3.WAL + busy_timeout:\n\n```\nconn.execute(\"PRAGMA journal_mode = WAL\")conn.execute(\"PRAGMA busy_timeout = 10000\")\n```\n\nWAL lets readers proceed during a write — which matters more since the CSV export, because a full export is a long read. WAL creates -wal and -shm siblings, so the directory must be writable: mount /data, never the single file.\n\nNon-root user, because phase 3 sets runAsNonRoot: true — build the habit early so the manifest needs no workaround.\n\nurllib healthcheck, because python:*-slim ships no curl.\n\n4. The .dockerignore bug — the most valuable finding in the project\n\nThe reference said:\n\n```\n*.db\n```\n\nAn agent probed it empirically: planted database/_probe.db, built, and found it inside an image layer. In this Docker/BuildKit version a bare *.db matches only at the build-context root, not recursively like .gitignore.\n\n```\n- *.db+ **/*.db+ **/*.db-wal+ **/*.db-shm\n```\n\nWhy this is severe: the .db files are gitignored but exist on disk in every working checkout, containing real user emails and password hashes. .gitignore does not filter a Docker build context — only .dockerignore does. A bare pattern plus a nested database equals credentials shipped in a published image.\n\nThe lesson: the agent did not trust the document. It *tested* the claim. That is the behaviour worth designing for.\n\n**But the deepest message isn’t actually about Docker**\n\nThis last sentence is the main lesson:\n\n“The agent did not trust the document. It tested the claim.”\n\nSuppose CLAUDE.md or another document says:\n\n```\n*.db prevents database filesfrom entering Docker images.\n```\n\nA weak AI workflow might do:\n\n```\nRead documentation       ↓Assume statement correct       ↓Report:\"Database files are protected.\" ✅\n```\n\nBut the intended AI-assisted engineering approach is:\n\n```\nRead documentation       ↓Form hypothesis       ↓Can I safely verify this?       ↓Create harmless _probe.db       ↓Build image       ↓Inspect result       ↓Evidence says assumption is wrong       ↓Fix .dockerignore       ↓Test again       ↓Report verified result\n```\n\nThe valuable behavior is:\n\nAI should verify important engineering assumptions with deterministic evidence whenever possible, rather than merely repeating documentation.\n\nRegion ap-southeast-1 (Singapore), account 149451857623, default VPC vpc-1ef5dd79.\n\nInstance: t3.micro, Ubuntu 24.04 LTS, IMDSv2 enforced, no key pair — SSM Session Manager only.\n\nSecurity group (sg-0741cb4e86b431320) — inbound:\n\nCompose binds 127.0.0.1:5001:5001. The prefix is load-bearing: \"5001:5001\" binds 0.0.0.0, and Docker's iptables rules bypass a UFW-style host firewall — the port ends up reachable even though ufw status says otherwise.\n\n**Other resources:**\n\nElastic IP eipalloc-056a8e1187edbb266 (allocated *before* certbot — a default public IP changes on stop/start and breaks both DNS and the cert)\n\n10 GB data volume vol-0024c08875df926b0 at /var/lib/spendly\n\nIAM instance profile scoped to one SSM parameter and one S3 prefix,\n\nS3 bucket for backups.\n\nTLS without owning a domain: 54-251-203-112.sslip.io. sslip.io resolves any embedded IP, and Let's Encrypt will issue for it. Zero cost, real certificate.\n\n**The trap that made TLS non-negotiable**\n\ncompose.yaml sets SPENDLY_ENV=production, and phase 2 ties SESSION_COOKIE_SECURE to it. Secure cookies are never transmitted over plain HTTP. Deploy behind nginx without TLS and we get a working-looking deploy where *nobody can log in*, with no error explaining why. Hence ProxyFix:\n\n```\nif os.environ.get(\"SPENDLY_BEHIND_PROXY\") == \"1\":    app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)\n```\n\nx_for=1 means \"trust exactly one proxy\"\n\nan app that trusts X-Forwarded-For while directly exposed lets any client spoof its own IP.\n\n**Backups: ****VACUUM INTO, never ****cp**\n\n```\nsqlite3 /var/lib/spendly/spendly.db \"VACUUM INTO '/tmp/backup.db'\"aws s3 cp /tmp/backup.db s3://spendly-backups/db/spendly-$(date -u +%FT%H%M%SZ).db\n```\n\nWith WAL active, cp spendly.db copies the main file and misses every transaction still in spendly.db-wal — a silently truncated backup that restores clean and is missing data.\n\nFull executed steps with real resource IDs: [deploy/vm/RUNBOOK.md](https://github.com/faizulkhan56/claude-setup-basic/blob/main/deploy/vm/RUNBOOK.md).\n\nIt is a record of an actual run, not a template — every command is followed by its real output (-> ami-0ed6a65b84536f6ce, -> i-0db2a335dd43fd951). Structure:\n\n```\n## Target## Resources created## Prerequisite code changes (2.1/2.2)## Step-by-step: AWS infrastructure provisioning     1. Latest Ubuntu 24.04 AMI via SSM parameter (never a hardcoded ID)     2. Pick a public subnet in the default VPC     3. S3 backup bucket — before the IAM role, so its policy scopes to a real ARN     4. Security group: 80/443 only, no 22     5. IAM role + instance profile — SSM, one parameter, one S3 prefix     6. Launch: t3.micro, IMDSv2 enforced, no key pair     7. Data disk — same AZ, attached at /dev/sdf     8. Elastic IP — allocate before use     9. Generate + store the Flask secret (once, never printed, never committed)## Step-by-step: configuring the VM\n```\n\nTwo habits from it worth copying:\n\nFetch AMI IDs from SSM, never hardcode:\n\n```\naws ssm get-parameter --name \\  /aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id \\  --query Parameter.Value --output text\n```\n\nVerify before trusting a default. The runbook confirms the data-disk device name before running bootstrap.sh:\n\n``` php\n-> confirmed: nvme0n1 (20G root), nvme1n1 (10G, unformatted) — matches\n```\n\nOn Nitro instances the device is /dev/nvme1n1, not the /dev/sdf you asked for. Mount by LABEL= with nofail — a wrong device path in /etc/fstab without nofail leaves the VM unbootable.\n\nOne correction we insisted on:\n\nan app running via python app.py on 127.0.0.1:5001 is \"running locally\", not \"deployed\". Calling it deployed implies an internet-facing app with the Werkzeug debugger exposed — remote code execution. In documentation, that distinction is not pedantry; it is the difference between a screenshot and a claim.\n\nThe earlier deployment chapter answers: How do we run Spendly safely?\n\nThis chapter answers the next operational question:\n\nWhen the application is running, can Claude help us understand what is happening from real telemetry without replacing Prometheus, Grafana, Loki, or deterministic alerting?\n\nThe current repository says yes, with one important boundary:\n\nClaude is a read-only correlation and explanation layer. Metrics, alert rules, logs, and dashboards remain the source of evidence.\n\nThis chapter is not inferred only from screenshots. The current main repository contains the complete control-plane wiring:\n\n```\n.claude/├── commands/│   └── observe-local.md├── agents/│   └── spendly-observability-analyst.md└── skills/    └── spendly-observability/        └── SKILL.md\n```\n\nIt also contains:\n\n```\nobservability/├── compose.yaml├── README.md├── prometheus/├── grafana/├── alloy/└── loki/...\n```\n\nAnd app.py currently implements opt-in RED instrumentation when SPENDLY_METRICS_ENABLED=1.\n\nThe application metrics are:\n\n```\nspendly_http_requests_totalspendly_http_request_duration_secondsspendly_http_requests_in_progress\n```\n\nThe Prometheus Python client also exposes process CPU/memory metrics. The application deliberately excludes /healthz, /readyz, and /metrics from the custom business-request RED metrics.\n\nThat detail prevents the monitoring system from measuring its own probe/scrape traffic as if it were user activity.\n\nThe repository’s observability/README.md and observability skill describe three incremental phases.\n\nPhase 1 — black-box availability\n\n```\n/healthz/readyz   ↓Blackbox Exporter   ↓Prometheus   ↓Grafana\n```\n\nSignals include:\n\nThe key distinction remains the same as in deployment:\n\n```\n/healthz = process is alive/readyz  = dependency-aware readiness\n```\n\nPhase 2 — application RED and process metrics\n\nSpendly then exposes request telemetry:\n\n```\nRateErrorsDuration\n```\n\nplus CPU and memory.\n\nPrometheus rules evaluate deterministic conditions such as:\n\nThis division of labor is fundamental:\n\nIf Prometheus can evaluate a condition mathematically, Prometheus should evaluate it. Do not ask an LLM every minute whether a threshold is bad.\n\nPhase 3 — centralized logs\n\nDocker stdout/stderr flows through Grafana Alloy into Loki:\n\n```\nDocker logs   ↓Grafana Alloy   ↓Loki   ↓Grafana\n```\n\nA collector being “up” is not proof that logs arrive. Verify the entire path:\n\n```\napplication emits log   ↓Alloy discovers source   ↓labels/process stages are correct   ↓Loki receives stream   ↓query returns the expected event\n```\n\nGrafana is provisioned with both Prometheus and Loki:\n\nand a combined dashboard provides metrics and logs in one operational view:\n\n```\nBrowser / test traffic        │        ▼   Spendly :5001      │      ├── /healthz ─────┐      ├── /readyz ──────┼──> Blackbox Exporter ──┐      └── /metrics ───────────────────────────────┼──> Prometheus                                                  │Docker stdout/stderr ──> Grafana Alloy ──> Loki  │                                      │           │                                      └─────┬─────┘                                            ▼                                          Grafana                                            │                                            ▼                                   Claude /observe-local                                            │                                            ▼                                read-only evidence report\n```\n\nThe AI is intentionally at the end of the chain:\n\n```\ninstrumentation   ↓collection   ↓storage   ↓query   ↓alert/visualize   ↓AI correlation\n```\n\nThe current command is intentionally small:\n\n```\ndescription: Analyze the local Spendly Prometheus/Grafana/Loki observability stackargument-hint: \"optional symptom, e.g. p95 latency increased\"allowed-tools: Read, Agent\n```\n\nIts body performs three steps.\n\nStep 1 — confirm the setup exists\n\nIt reads:\n\n```\nobservability/README.md.claude/skills/spendly-observability/SKILL.md\n```\n\nIf either is missing, the workflow stops.\n\nStep 2 — delegate to the specialist\n\nIt invokes:\n\n```\nspendly-observability-analyst\n```\n\nand tells the analyst to:\n\nStep 3 — return a plain-language report\n\nThe command asks for:\n\n```\ncurrent statemetric and log evidenceassessmentcorrelationlikely cause only if supportednext safe verification steprecommended remediation — not executed\n```\n\nThis is a clean command → agent → skill reuse of the architecture developed earlier in the guide.\n\nThe agent is not merely told “please be careful.” Its allowed tools are narrow:\n\n```\nReadGrepGlobcurldocker compose ... psdocker compose ... logsSkill\n```\n\nIt is explicitly forbidden from:\n\n```\nupdownrestartrmexeckillvolume deletionimage rebuildconfig editspackage installscloud commands\n```\n\nThat means observation and remediation are structurally separated.\n\nThe skill and agent define a concrete order.\n\n```\ndocker compose --env-file observability/.env -f observability/compose.yaml ps\n```\n\n2. Prometheus target and black-box health\n\n```\nupprobe_success{job=\"spendly-blackbox\"}ALERTS{alertstate=\"firing\"}\n```\n\n3. Request rate and errors\n\n```\nsum(rate(spendly_http_requests_total[1m]))\nsum by (route,status) (  rate(spendly_http_requests_total[5m]))\n```\n\n4. Application p95\n\n```\nhistogram_quantile(  0.95,  sum by (le,route) (    rate(spendly_http_request_duration_seconds_bucket[5m])  ))\n```\n\n5. Process pressure\n\n```\nprocess_resident_memory_bytes{job=\"spendly-app\"}\nrate(process_cpu_seconds_total{job=\"spendly-app\"}[5m])\n```\n\n6. Loki logs over a matching window\n\n```\n{stack=\"spendly-observability\",service_name=\"spendly\"}\n```\n\n7. Bounded Compose logs only when a hypothesis needs confirmation\n\n```\nspendlyprometheuslokialloy\n```\n\nThis order matters. It moves from broad availability toward narrower internal evidence instead of jumping directly to a favorite root cause.\n\nThe repository now documents the actual user-facing prompt progression. These are better evidence than reconstructing a remembered chat from scratch.\n\nStart with the command alone:\n\n```\n/observe-local\n```\n\nBecause no symptom is supplied, the command asks the analyst for a general health/readiness/request-rate/error/p95/process/log-pipeline assessment.\n\nConceptually Claude does:\n\n```\nstack state   ↓up + black-box health/readiness   ↓request rate/errors/p95   ↓CPU/memory   ↓firing alerts   ↓Loki pipeline/logs   ↓overall assessment\n```\n\nUse this first. It establishes whether the monitoring stack itself is trustworthy before investigating a specific incident.\n\nThe repo’s documented example is:\n\n```\n/observe-local explain whether the application is healthy and correlate metrics with the last 15 minutes of Spendly logs\n```\n\nThis changes the task from “show health” to “prove the assessment from two signal families.”\n\nClaude should:\n\n```\nquery health/readiness + RED/process metrics   ↓query Spendly Loki logs for the same 15-minute window   ↓compare timestamps   ↓state what both sources agree on   ↓call out any conflicting signal\n```\n\nThis is the first real correlation prompt.\n\nThe repo’s latency test prompt is:\n\n```\n/observe-local investigate why p95 latency increased and show the Prometheus evidence plus any matching Spendly log evidence\n```\n\nThe analysis should not begin with “the database is slow.” It should begin with:\n\n```\nIs internal p95 actually elevated?Which route is elevated?Is black-box latency also elevated?Did error rate change?Did CPU/memory move?Are there matching logs in the same period?\n```\n\nOnly when supporting signals line up should the report name a likely cause.\n\nThis is the prompt represented by the latency-correlation evidence:\n\nThe repository also documents:\n\n```\n/observe-local check for firing alerts, 5xx errors, readiness problems, and relevant Loki logs\n```\n\nThis prompt is useful when the operator does not yet know whether the incident is availability, application failure, or an observability-pipeline problem.\n\nIt asks Claude to compare:\n\n```\nALERTS   +5xx route/status series   +health/readiness probes   +Loki evidence\n```\n\nA missing log stream alone must not be called an application outage; Alloy and Loki health must be checked first.\n\nThe shorter /observe-local ... examples above are the actual repository-documented inputs. When we want to make the output contract explicit in a manual Claude CLI session, this fuller form is useful:\n\n```\nUse the Spendly observability workflow and analyze the current application behavior.\nCheck Prometheus metrics and Loki logs for the same time window.\nFocus on:- request rate,- errors,- latency,- process health,- firing rules,- matching log events.\nDo not change code, containers, dashboards, alert rules, or infrastructure.\nReturn:1. current state,2. exact metric evidence,3. exact log evidence,4. correlation between the two,5. most likely cause only if supported,6. confidence and remaining uncertainty,7. one next safe verification step,8. recommended remediation, not executed.\n```\n\nThis is not a replacement for the slash command; it explains the contract the command/agent/skill already enforce.\n\nThe analyst definition standardizes the output:\n\n```\nSpendly AI-Assisted Observability ReportCurrent state- containers- black-box health/readiness- Prometheus target health- request rate / errors / p95- process CPU / memory- firing rules- Loki/Alloy log pipelineEvidence- exact metric values, labels, time window, relevant log factsAssessment- HEALTHY- DEGRADED- UNAVAILABLE- OBSERVABILITY STACK ISSUECorrelation- what metrics and logs agree on- conflicts still needing explanationMost likely cause- evidence-backed only- otherwise: \"not isolated yet\"Next safe verification step- one read-only query/commandRecommended remediation- describe only; do not execute\n```\n\nThe full correlation screenshot captures the same philosophy:\n\nA reliable test is not:\n\n```\nGrafana opens\n```\n\nor:\n\n```\nAlloy container is running\n```\n\nIt is:\n\n```\napplication generates behavior   ↓Prometheus receives metrics   ↓rules evaluate   ↓Alloy receives logs   ↓Loki stores/query returns logs   ↓Grafana exposes independent human evidence   ↓Claude queries matching windows   ↓Claude reports evidence + correlation + uncertainty\n```\n\nThis mirrors verify_setup.py at runtime: verify the wiring, not only the existence of components.\n\nThe current skill includes several high-value rules.\n\n```\nhealth=1, ready=0→ process/network alive; DB-aware readiness failing→ verify evidence before naming DB root cause\nblack-box latency high + internal p95 normal→ inspect probe/network path before application code\ninternal p95 high + black-box health normal→ app is available but slower→ narrow by route and logs\n5xx ratio high→ identify route/status series→ query Loki over the same window\nGrafana \"no data\"→ not proof of outage→ query Prometheus/Loki APIs directly\nLoki \"no data\"→ may be Alloy discovery/socket/config issue→ verify log pipeline before judging app health\n```\n\nAnd the most important sentence:\n\nIf evidence is insufficient, saynot isolated yet.\n\nThat phrase is a feature, not a failure.\n\nCorrelation without time alignment is storytelling.\n\nBad:\n\n```\nlatency spike at 14:05+unrelated error log at 09:10=invented root cause\n```\n\nBetter:\n\n```\nroute p95 rises around 14:05+5xx/CPU/readiness checked around 14:05+matching application logs around 14:05=evidence-backed hypothesis\n```\n\nThe analyst is explicitly told to align metrics and logs to the same window.\n\nClaude does not become the metric database, alert engine, log store, or final root-cause authority.\n\n```\n1. Prometheus/Grafana exposes a deterministic signal.2. Select a time window.3. Run /observe-local with the symptom.4. Claude queries metrics and logs read-only.5. Claude returns:   - current state,   - evidence,   - correlation,   - hypothesis,   - uncertainty,   - next verification.6. Human validates the hypothesis.7. Remediation goes through the normal feature/DevOps workflow.8. Monitoring verifies recovery.\n```\n\nDo not collapse steps 3 and 7 into one autonomous “diagnose and fix production” agent. Separating diagnosis from mutation preserves evidence and keeps blast radius bounded.\n\nThe repository currently correlates metrics and centralized container logs.\n\nIt does not yet implement distributed tracing.\n\nThat means the next logical phase is OpenTelemetry tracing/service-dependency analysis, added separately so the metrics/logging system remains understandable and reversible.\n\nThe order should be:\n\n```\nmetrics + logs proven   ↓add tracing   ↓verify trace propagation   ↓then extend Claude correlation to metric + log + trace\n```\n\nThe numbers in this project are useful, but they measure different dimensions and should not be collapsed into a single “quality score.”\n\nA mature project watches trends rather than chasing raw counts. For example, adding 50 low-value tests can increase the test count while reducing maintainability. A better metric pair is coverage of critical contracts + failure-detection quality.\n\nThe most important outcome reported here is qualitative: the workflow caught a real CSV injection risk before merge and several wiring/configuration defects before they became runtime surprises. That demonstrates the value of layered independent checks.\n\nThe failures taught more than the successes. Every one is a real event from this project.\n\n1. Five sibling skills cost 932 always-on tokens. Collapsed to one skill with references/ → 146. *Prefer progressive disclosure.*\n\n2. A skill directory name did not match its frontmatter name. It never registered — silently. *verify_setup.py now checks this.*\n\n3. Both reviewers had only Bash(git diff). A feature adding new files produces an empty diff, so they reported \"no findings\" on unreviewed work. *Added **Bash(git status) and explicit enumeration.*\n\n4. The test-writer’s documented fixture used a username field that does not exist, and set app.config['DATABASE'], which the app never reads — so tests would have run against the developer's real database. *Replaced with the actual **DB_PATH-patch pattern.*\n\n5. Three tests asserted the wrong contract. They checked a SQL-injection payload returned zero rows — but a parameterised query binds it as a literal, and ' (ASCII 39) sorts below 2 (50), so BETWEEN legitimately matches everything. *Now assert the call returns and the schema survives.*\n\n6. protect_paths.py shipped five false positives, including blocking every commit with a Co-Authored-By trailer. *48 tests now pin them.*\n\n7. We nearly wrote python3 hook.py || python hook.py — which silently disables a blocking guard, because exit 2 reads as failure and the retry sees consumed stdin. *Documented as a comment so nobody re-adds it.*\n\n8. .dockerignore used bare *.db, which matches only at the build-context root — a nested database leaked into an image layer with real password hashes. ***/*.db.*\n\n9. A subagent died on an API error and reported Done with 0 tool uses. *Treat **0 tool uses as failure.*\n\n10. Two sessions shared one working tree while a backgrounded agent was mid-edit. *Keep the observing session read-only.*\n\n11. CLAUDE.md drifted, repeatedly. Five implemented routes still marked \"stub\". database/db.py described as empty when it had six functions. queries.py absent from the architecture tree entirely. A test baseline stale in five files at once. A /deploy-phase pre-flight excusing three failures as \"known\" long after they were fixed.\n\nThat last one is the thesis of this guide. In a normal project, stale docs are untidy. Here, six subagents read CLAUDE.md as ground truth — so a stale fact does not sit inert, it *propagates into decisions*. The pre-flight excusing fixed failures would have let a real regression through on the next run.\n\nWhich is why the most valuable file in the whole setup is the 150-line script that checks whether the documentation is still true.\n\n```\npython .claude/verify_setup.py     # 56 checks; non-zero exit on any break\n```\n\nWhen several Claude Code features seem capable of solving the same problem, start with the smallest mechanism that provides the guarantee you need.\n\n```\nIs this a stable fact every session needs?  └─ yes → CLAUDE.mdIs this reusable knowledge/procedure needed only for certain tasks?  └─ yes → Skill (+ references for large detail)Is this a repeatable multi-step workflow with gates/order?  └─ yes → Slash commandDoes the task need isolated context, specialist role, or narrower tools?  └─ yes → SubagentMust something happen automatically at a lifecycle event?  └─ yes → HookDoes Claude need a structured capability from another process/service?  └─ yes → MCP serverDo several repositories/users need the same bundle?  └─ yes → Plugin + marketplaceMust a rule be authoritative even outside Claude Code?  └─ yes → CI/CD, IAM, branch protection, policy engine, or other external controlNeed deterministic runtime metrics/logs?  └─ yes → Prometheus / Alloy / Loki / GrafanaNeed evidence-based cross-signal explanation without mutation?  └─ yes → read-only observability command → analyst agent → observability skill\n```\n\nDo not create a hook, skill, reviewer agent, and MCP tool for the same rule just because all are available. Layer them only when each layer contributes a distinct property.\n\nA security example:\n\n```\nHook  → blocks known dangerous command patterns immediatelySecurity skill  → provides the review methodology and checklistSecurity reviewer agent  → performs independent cross-file reasoning on high-risk changesCI scanner / branch gate  → enforces organization policy independent of local Claude configuration\n```\n\nThat is layered defence, not duplication, because each component has a different trigger and guarantee.\n\nA reliable Claude Code repository is not one giant prompt. It is a small system of specialized components:\n\n```\n                Human intent                    │                    ▼             Project knowledge                CLAUDE.md                    │          ┌─────────┴─────────┐          ▼                   ▼       Command            Direct task          │          ▼        Agent  ────────────────┐          │                    │          ▼                    │        Skill                  │          │                    │          ▼                    │   references / tools          │          │                    │          └──────────┬─────────┘                     ▼                Tool actions                     │               Hooks surround              lifecycle events                     │                     ▼             Code / infrastructure                     │                     ▼              CI and external             policy validate it\n```\n\nThe design goal is not maximum automation. It is predictable automation with clear contracts, bounded authority, observable failures, and cheap ways to verify that the wiring still matches reality.\n\nThe complete implementation, including the Claude Code configuration, agents, skills, hooks, commands, DevOps workflows, and AI-assisted observability experiments discussed in this article, is available in my project repository:\n\n**Project repository:**\n\n[faizulkhan56/claude-setup-basic — AI-assisted Spendly implementation](https://github.com/faizulkhan56/claude-setup-basic?utm_source=chatgpt.com)\n\nThis work started from the Spendly project created by CampusX, which I forked and then used as the foundation for exploring and extending a properly wired Claude Code engineering workflow.\n\n**Original project:**\n\n[campusx-official/spendly — An expense tracking application built using Claude Code](https://github.com/campusx-official/spendly?utm_source=chatgpt.com)\n\nA special thanks to [CampusX](https://github.com/campusx-official?utm_source=chatgpt.com) and **Nitish Singh** for creating and sharing such a useful Claude Code–based project.\n\n[Claude Code, Properly Wired: A One-Stop Guide to Spec-Driven Development on a Real Project](https://pub.towardsai.net/claude-code-properly-wired-a-one-stop-guide-to-spec-driven-development-on-a-real-project-e677fad8d793) was originally published in [Towards AI](https://pub.towardsai.net) on Medium, where people are continuing the conversation by highlighting and responding to this story.", "url": "https://wpnews.pro/news/claude-code-properly-wired-a-one-stop-guide-to-spec-driven-development-on-a-real", "canonical_source": "https://pub.towardsai.net/claude-code-properly-wired-a-one-stop-guide-to-spec-driven-development-on-a-real-project-e677fad8d793?source=rss----98111c9905da---4", "published_at": "2026-08-26 04:58:37+00:00", "updated_at": "2026-08-26 05:12:48.650704+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools", "artificial-intelligence"], "entities": ["Claude Code", "Spendly", "Flask", "SQLite", "AWS EC2"], "alternates": {"html": "https://wpnews.pro/news/claude-code-properly-wired-a-one-stop-guide-to-spec-driven-development-on-a-real", "markdown": "https://wpnews.pro/news/claude-code-properly-wired-a-one-stop-guide-to-spec-driven-development-on-a-real.md", "text": "https://wpnews.pro/news/claude-code-properly-wired-a-one-stop-guide-to-spec-driven-development-on-a-real.txt", "jsonld": "https://wpnews.pro/news/claude-code-properly-wired-a-one-stop-guide-to-spec-driven-development-on-a-real.jsonld"}}