Why Your Claude-Assisted Builds Break Down After Week 3 (And How to Fix It)

There’s a pattern I keep seeing with developers who use Claude to build things.

Week 1: Everything’s flying. Features ship fast. It feels like a superpower.

Week 2: Still fast, but you’re starting to notice context is getting messy.

Week 3: Something breaks. You ask Claude to fix it. Claude fixes it and breaks something else. You’re now spending more time explaining what was already built than actually shipping.

By week 4, some people stop using Claude entirely. Others power through but feel stuck in a loop.

Here’s what’s happening — and how to get out of it.

The real problem isn’t your prompts

Most people assume they’re bad at prompting. They go looking for better prompt templates, prompt chaining techniques, or ways to “jailbreak” better responses.

That’s the wrong diagnosis.

The actual problem is structural debt.

Every time you give Claude a new instruction without grounding it in how your codebase actually works, you’re building on a shaky foundation. Claude doesn’t automatically know:

  • How your files are organized
  • What decisions you made three sessions ago
  • Why certain patterns exist
  • What naming conventions you’ve established
  • What’s intentionally simple vs. what’s genuinely incomplete

Without that context, Claude is essentially building in the dark. It produces output that works locally but doesn’t integrate cleanly with what already exists.

The trust vs. verify trap

There’s a related issue: AI is fast, and that speed creates false confidence.

When you get a working feature in 30 seconds, your brain treats it like code you fully understand. But you didn’t write it — you approved it. And those are very different levels of comprehension.

The structural problems you’d normally catch while writing the code (because you were forced to think through each decision) don’t get caught because you’re skimming output instead of authoring it.

This isn’t a critique of Claude — it’s a workflow problem. The tool is doing exactly what you asked. The issue is that “write this feature” is a very different request from “write this feature in a way that’s consistent with how everything else in this project is built.”

What actually helps

The shift I’ve seen work best is this: stop treating each session as a fresh conversation.

Instead of explaining what you want Claude to build, start each session by grounding it in what already exists. Give it:

  • A brief architecture summary (what this codebase is, what patterns it follows)
  • The relevant existing code it needs to understand before making changes
  • Clear constraints (“don’t create new files when you can extend existing ones”)
  • Explicit acceptance criteria (“this is done when X, Y, Z are true”)

This sounds like more work upfront, but it dramatically reduces the rework cycle. You’re paying for context now instead of paying for debugging later.

The CLAUDE.md pattern

One practical technique: create a CLAUDE.md file at your project root. It doesn’t need to be long — even 20-30 lines can make a meaningful difference.

Include things like:

  • The project’s purpose in one sentence
  • Key architectural decisions and why you made them
  • Naming conventions
  • What Claude should avoid doing (avoid creating new API layers, prefer extending existing services, etc.)

When you start a new session, paste that file in before you start giving instructions. You’ll immediately notice Claude’s responses becoming more consistent and less likely to introduce patterns that don’t fit your codebase.

The deeper mental model shift

The builders who get consistent results with Claude aren’t necessarily better at writing prompts.

They’ve internalized that Claude is a collaborator that needs orientation, not a vending machine that dispenses features. Every time they start a session, they think: what does Claude need to know about this codebase before I give it a task?

That shift — from “request features” to “maintain shared context” — is what separates builds that compound over time from ones that become impossible to maintain.

A free resource

I built a short starter pack around this idea — 9 pages, completely free. It covers the core mental model shift, includes 5 prompt frameworks from a larger system I’ve developed, and shows how to structure your workflow so Claude builds things that stay maintainable.

If you’re hitting the week-3 wall, it might help: Ship With Claude — Starter Pack

No email required, no upsell. Just the thing that helped me.

What patterns have you noticed in your own AI-assisted builds? Curious what’s worked (or hasn’t) for people at different scales.

High p99 Latency in Go Service: Identifying and Resolving Bottlenecks to Prevent System Overload

cover

cover

Introduction: The Latency Challenge

In distributed systems, p99 latency often emerges as the silent killer of performance, despite healthy p50 and p95 metrics. This phenomenon is particularly acute in Go services, where the request lifecycle—from client initiation to load balancer routing and service processing—can be disrupted by straggler requests. These stragglers, consuming disproportionate resources, act as systemic bottlenecks, delaying subsequent requests and cascading into degraded user experience. The mechanical process here is straightforward: a single slow request, often due to resource contention or downstream dependency issues, holds up the goroutine scheduler, causing a backlog that amplifies tail latency.

Retries, a common mitigation strategy, proved ineffective—and in some cases, counterproductive. The causal chain is clear: retries increase load on already stressed resources, triggering retry storms that exacerbate latency. This is particularly evident in Go’s runtime, where garbage collection pauses and network variability compound the issue. The failure mechanism here is twofold: first, retries assume the problem is transient, but stragglers are often persistent; second, they lack awareness of system load, blindly adding pressure without resolving the root cause.

The breakthrough came from reframing the problem: stragglers, not failures, were the enemy. Hedged requests—sending a backup request after a timeout—emerged as a solution. The key challenge was determining the optimal hedging timeout without overloading the system. This decision point is critical: too short, and you waste resources on redundant requests; too long, and you miss the window to mitigate the straggler. The optimal timeout depends on queueing theory principles, balancing service time and request distribution to minimize tail latency without triggering resource exhaustion.

In practice, hedging reduced p99 latency by 74% in a real-world Go service, with minimal impact on p50 and a slight, expected increase in load. The implementation, packaged as hedge, demonstrates the effectiveness of this approach. However, hedging is not a silver bullet. It fails when downstream dependencies are the bottleneck, as parallel requests can saturate shared resources. The rule for choosing hedging is clear: if stragglers dominate tail latency and retries worsen load, use hedging with a timeout tuned to your request distribution. Missteps include over-triggering hedges, leading to unintended load spikes, and ignoring root causes like inefficient code or misconfigured load balancers.

Understanding the physical mechanics of latency—how requests queue, resources contend, and timeouts trigger—is essential. Without this, solutions remain superficial, addressing symptoms rather than causes. The stakes are high: unchecked p99 latency leads to system instability, operational inefficiencies, and user abandonment. As systems grow more complex, hedging stragglers offers a practical, measurable path to resilience—but only when applied with precision and awareness of the underlying mechanisms.

Diagnosing the Root Cause

Identifying the source of high p99 latency in our Go service required a systematic approach, combining trace analysis, statistical correlation, and controlled experimentation. The investigation revealed surprising insights into how straggler requests—not failures—were the primary culprits, despite initial assumptions.

Step 1: Isolating the Straggler Phenomenon

Using distributed tracing, we instrumented requests to map their lifecycle from client to response. The data showed that while p50 and p95 latencies were stable, p99 requests exhibited erratic behavior, often exceeding thresholds by orders of magnitude. These stragglers were not isolated incidents but systemic bottlenecks, consuming disproportionate CPU and memory resources due to Go’s goroutine scheduler being held up by slow requests.

Key observation: Retries exacerbated the issue, increasing load without resolving the root cause. This was confirmed by load testing, where retry storms triggered garbage collection pauses, further amplifying tail latency.

Step 2: Correlating Latency with Resource Contention

We employed statistical analysis to correlate latency spikes with resource utilization metrics. The results were striking: stragglers coincided with CPU saturation and network congestion, particularly in downstream dependencies. However, the surprising finding was that these issues were not due to failures but to persistent slow requests—a form of resource contention masked as transient errors.

Mechanism: Slow requests forced subsequent requests into a queueing backlog, where they waited for resources, creating a cascading delay effect. This was exacerbated by Go’s runtime characteristics, where goroutines blocked on I/O or synchronization primitives, stalling the scheduler.

Step 3: Testing Hedging vs. Retries

To address stragglers, we compared two strategies: retries and hedged requests. Retries, while intuitive, failed due to their blind addition of load without system load awareness. Hedging, however, reframed the problem by parallelizing backup requests after a timeout, effectively bypassing stragglers.

Experiment setup: We implemented hedging with a timeout threshold determined by queueing theory, balancing service time and request distribution. The results were dramatic—a 74% reduction in p99 latency with minimal impact on p50 and a slight, expected increase in load.

Edge case: Hedging failed when downstream dependencies were the bottleneck, as parallel requests saturated shared resources. This highlighted the importance of system load awareness in hedging timing.

Step 4: Validating the Solution with Chaos Engineering

To ensure robustness, we injected controlled stragglers using chaos engineering. The hedging strategy consistently outperformed retries, maintaining system stability under stress. However, we identified a critical failure mode: over-triggering hedges due to misconfigured timeouts, leading to unintended load spikes.

Rule for hedging: If stragglers dominate tail latency and retries worsen load, use hedging with a timeout tuned to request distribution. Avoid over-triggering by monitoring resource utilization and request queue lengths.

Surprising Findings and Practical Insights

  • Stragglers, not failures, are the primary drivers of tail latency. Retries are ineffective or counterproductive in these cases.
  • Hedging is a nuanced solution, requiring precise timeout thresholds and awareness of downstream dependencies.
  • Understanding the physical mechanics of latency—queuing, resource contention, and timeout triggers—is essential for effective solutions.

By addressing stragglers through hedged requests, we not only reduced p99 latency but also gained deeper insights into the systemic issues underlying tail latency in distributed systems.

Key Insights and Surprising Discoveries

The journey to reducing p99 latency by 74% in a Go service revealed several counterintuitive insights that challenged conventional wisdom. Here’s what stood out—and why it matters.

1. Stragglers, Not Failures, Drive Tail Latency

The most surprising discovery was that straggler requests, not transient failures, were the primary cause of high p99 latency. While retries are commonly used to handle failures, they exacerbated the problem by increasing load without addressing the root cause. Stragglers—requests that consume disproportionate resources due to resource contention or downstream dependency issues—create a queueing backlog. This backlog cascades delays, as Go’s goroutine scheduler becomes blocked by slow requests, stalling subsequent processing.

Mechanism: Slow requests hold onto CPU and memory, preventing the scheduler from efficiently allocating resources to other goroutines. This contention amplifies tail latency, as the system struggles to recover from the backlog.

2. Hedging Outperforms Retries by Reframing the Problem

The effectiveness of hedged requests was unexpected. By sending a backup request after a timeout, hedging addresses stragglers directly, rather than blindly retrying. This approach reduced p99 latency by 74% while leaving p50 mostly unchanged. The key was tuning the hedging timeout using queueing theory to balance service time and request distribution.

Mechanism: Hedging parallelizes the handling of slow requests, reducing the time spent waiting for stragglers. However, it fails when downstream dependencies are the bottleneck, as parallel requests saturate shared resources, leading to resource exhaustion.

3. Retries Are a Blunt Tool—and Often Counterproductive

Retries, while intuitive, were found to be ineffective or harmful. They assume transient issues but lack awareness of system load, often triggering retry storms that worsen latency. In Go, retries also coincide with garbage collection pauses, further degrading performance.

Mechanism: Retries add load without resolving the underlying straggler issue, leading to a feedback loop where increased load slows down the system further. This is particularly problematic in Go due to its runtime characteristics.

4. Optimal Hedging Timeout Is Critical—and Fragile

The success of hedging hinges on the timeout threshold. Too short, and it triggers unnecessary backup requests; too long, and it fails to mitigate stragglers. The optimal timeout depends on the request distribution and service time, requiring careful tuning.

Mechanism: The timeout must account for the point at which a request is likely to become a straggler. This is determined by analyzing the latency distribution and identifying the inflection point where requests deviate from typical performance.

5. Monitoring Resource Utilization Is Non-Negotiable

Hedging introduces a slight increase in load, which is expected. However, without monitoring CPU saturation, memory usage, and queue lengths, hedging can lead to unintended load spikes. This is especially risky in production, where resource limits are stricter.

Mechanism: Over-triggering hedges saturates resources, negating the benefits of reduced tail latency. Monitoring ensures that hedging remains within safe bounds, avoiding resource exhaustion.

Rule for Hedging: When to Use It—and When to Avoid

Use hedging if:

  • Stragglers dominate tail latency, and retries worsen load.
  • The hedging timeout is tuned to the request distribution.
  • Resource utilization is monitored to avoid over-triggering.

Avoid hedging if:

  • Downstream dependencies are the bottleneck, as parallel requests will saturate shared resources.
  • Root causes like inefficient code or misconfigured load balancers are unresolved.

Hedging is not a silver bullet but a precise tool for addressing stragglers. Its effectiveness depends on understanding the physical mechanics of latency—queuing, resource contention, and timeout triggers. When applied correctly, it transforms tail latency from a systemic issue into a manageable problem.

Implementation and Results

Addressing the high p99 latency in our Go-based service required a shift from traditional retry mechanisms to a more nuanced approach: hedged requests. The core issue wasn’t transient failures but straggler requests—slow responses that disproportionately consumed resources, blocking the goroutine scheduler and creating a queueing backlog. Retries, while intuitive, exacerbated the problem by increasing load and triggering garbage collection pauses in Go’s runtime, leading to a feedback loop of slower performance.

Hedging Strategy: Parallelizing Stragglers

We implemented hedged requests by sending a backup request if the primary request exceeded a calculated timeout. This timeout was determined using queueing theory, balancing the service time distribution and request latency inflection points. The mechanism works as follows:

  • Trigger Condition: A backup request is initiated when the primary request’s latency exceeds the calculated timeout, identified via latency distribution analysis.
  • Resource Impact: Parallel requests increase CPU and network load slightly, but this is offset by the reduction in tail latency. Monitoring resource utilization (CPU saturation, memory usage, and queue lengths) ensured hedging remained within safe bounds.
  • Outcome: This approach reduced p99 latency by 74%, with p50 remaining largely unchanged. The slight increase in load was expected and manageable, as hedging targeted only stragglers, not all requests.

Comparing Hedging vs. Retries: A Causal Analysis

Retries and hedging address different root causes. Retries assume transient failures, blindly adding load without resolving stragglers. Hedging, however, parallelizes handling of slow requests, reducing wait time for stragglers. The effectiveness of each depends on the system’s failure mode:

  • Retries: Optimal for transient network issues or intermittent failures. However, in our case, retries worsened latency by triggering retry storms and coinciding with garbage collection pauses.
  • Hedging: Optimal when stragglers dominate tail latency. It fails when downstream dependencies are the bottleneck, as parallel requests saturate shared resources.

Rule for Hedging: Use hedging if stragglers dominate tail latency and retries worsen load. Tune timeout to request distribution and monitor resource utilization to avoid over-triggering.

Edge Cases and Limitations

Hedging is not a silver bullet. It fails in scenarios where:

  • Downstream Bottlenecks: Parallel requests saturate shared resources (e.g., a database), negating latency benefits.
  • Misconfigured Timeouts: Over-triggering hedges due to poorly tuned timeouts causes unintended load spikes, amplifying resource exhaustion.

To mitigate these risks, we validated the hedging strategy through chaos engineering, injecting controlled stragglers to test system resilience. This revealed that hedging outperformed retries under stress but required precise timeout tuning and resource monitoring.

Practical Implementation and Broader Impact

The hedging mechanism was packaged into a reusable library, hedge, with minimal integration required:

client := &http.Client{ Transport: hedge.New(http.DefaultTransport),}resp, err := client.Get("https://api.example.com/data")

The broader impact of this change extended beyond latency reduction. By addressing stragglers, we improved system stability, reduced operational costs associated with inefficiencies, and enhanced user experience. Unchecked p99 latency had previously led to cascading delays, causing system instability and user abandonment. With hedging, the system became more resilient to tail latency spikes, maintaining competitive performance even under load.

Key Takeaways

  • Stragglers Drive Tail Latency: Focus on slow requests, not failures, to reduce p99 latency.
  • Hedging Requires Precision: Tune timeouts using queueing theory and monitor resource utilization to avoid over-triggering.
  • Retries Are Counterproductive: Address root causes (e.g., stragglers) instead of blindly adding load.

By understanding the physical mechanics of latency—request queuing, resource contention, and timeout triggers—we transformed a systemic issue into a measurable improvement, setting a new standard for performance optimization in distributed systems.

Lessons Learned and Best Practices

After dissecting the mechanics of high p99 latency in Go services, several actionable lessons emerged. These aren’t generic tips—they’re grounded in the physical and mechanical processes of request handling, resource contention, and system behavior under stress.

1. Stragglers, Not Failures, Drive Tail Latency

The root cause of high p99 latency in our case was straggler requests, not transient failures. Stragglers consume disproportionate resources (CPU, memory) due to Go’s goroutine scheduler being blocked by slow I/O or synchronization operations. This creates a queueing backlog, delaying subsequent requests. Rule: If p99 latency spikes while p50 remains stable, investigate stragglers before blaming failures.

2. Retries Worsen Latency by Amplifying Load

Retries are a blunt tool. In our case, they triggered retry storms, coinciding with garbage collection pauses in Go’s runtime. This created a feedback loop: more retries → more load → slower GC → higher latency. Rule: Avoid retries if stragglers dominate tail latency. Instead, address the root cause of slowness.

3. Hedging Outperforms Retries—But Only with Precision

Hedged requests reduced p99 latency by 74% by parallelizing backup requests after a calculated timeout. The key is tuning the timeout using queueing theory, balancing service time and request distribution. Rule: Use hedging if stragglers dominate and retries worsen load. Timeout must be precise; over-triggering hedges causes unintended load spikes. Edge Case: Hedging fails when downstream dependencies are the bottleneck, as parallel requests saturate shared resources.

4. Monitor Resource Utilization to Avoid Over-Triggering

Hedging increases load slightly, but without monitoring, it risks resource exhaustion. Over-triggering hedges saturates CPU, memory, or network, negating latency benefits. Rule: Monitor CPU saturation, memory usage, and queue lengths to keep hedging within safe bounds.

5. Understand the Physical Mechanics of Latency

Latency isn’t abstract—it’s a result of request queuing, resource contention, and timeout triggers. For example, Go’s goroutine scheduler stalls when slow requests block I/O, amplifying tail latency. Rule: Analyze latency distribution and correlate spikes with resource metrics to identify root causes.

6. Validate Solutions with Chaos Engineering

We validated hedging by injecting controlled stragglers and comparing it to retries under stress. Hedging outperformed retries but required precise timeout tuning and resource monitoring. Rule: Test solutions in realistic stress scenarios to uncover edge cases and limitations.

Practical Implementation

To apply these lessons, use the hedge library (available at https://github.com/bhope/hedge):

  • Minimal Integration: Wrap your HTTP client with hedging transport.
  • Timeout Tuning: Analyze latency distribution to identify straggler inflection points.
  • Monitoring: Track CPU, memory, and queue lengths to avoid over-triggering.

When Hedging Fails

Hedging isn’t a silver bullet. It fails when:

  • Downstream dependencies are the bottleneck: Parallel requests saturate shared resources.
  • Timeouts are misconfigured: Over-triggering hedges causes load spikes.
  • Root causes are ignored: Inefficient code or misconfigured load balancers persist.

Final Rule of Thumb

If stragglers dominate tail latency and retries worsen load → use hedging with precise timeout tuning and resource monitoring. Otherwise, address root causes like inefficient code or downstream bottlenecks.

Unchecked p99 latency leads to system instability, operational inefficiencies, and user abandonment. By understanding the mechanics and applying these lessons, you can transform latency issues into measurable performance optimizations.

Conclusion: The Path Forward

Digging into the root cause of high p99 latency in Go services revealed a surprising truth: stragglers, not failures, are the primary culprits. Traditional retries, often the go-to solution, exacerbated the problem by triggering retry storms and coinciding with garbage collection pauses, creating a feedback loop of increased load and slower performance. This is because retries add more requests to an already overloaded system, further blocking Go’s goroutine scheduler and amplifying tail latency.

The hedging strategy emerged as a superior alternative, reducing p99 latency by 74% in our case study. By parallelizing backup requests after a calculated timeout, hedging effectively short-circuits stragglers before they cascade into system-wide delays. However, its success hinges on precise timeout tuning—a misstep here can lead to over-triggering, saturating resources and negating latency gains. This is where queueing theory becomes indispensable, helping to identify the inflection point where a request transitions from “slow” to “straggler.”

Yet, hedging isn’t a silver bullet. It fails when downstream dependencies are the bottleneck, as parallel requests can saturate shared resources like databases. In such cases, addressing the root cause—whether inefficient code, misconfigured load balancers, or downstream bottlenecks—is paramount. Resource monitoring is equally critical; without tracking CPU saturation, memory usage, and queue lengths, hedging risks becoming a liability rather than a solution.

The key takeaway? Tail latency is a symptom of systemic issues, not isolated failures. Resolving it requires a curious, systematic mindset—one that combines trace analysis to pinpoint bottlenecks, chaos engineering to validate solutions under stress, and statistical analysis to correlate latency spikes with resource utilization. By approaching performance challenges with this rigor, we can transform latency issues into measurable optimizations, ensuring systems remain stable, efficient, and responsive even under load.

Rule for Hedging: Use hedging if stragglers dominate tail latency and retries worsen load. Tune timeout to request distribution, monitor resource utilization, and avoid if downstream dependencies are the bottleneck.

The path forward is clear: deep investigation pays off. By understanding the mechanics of latency, we can craft solutions that not only address symptoms but also fortify the system against future challenges. The potential for improvement is vast—and it starts with asking the right questions.

Ghost Maintainer — An AI Junior Partner for Open Source

Ghost Maintainer Notion Page

This is a submission for the Notion MCP Challenge

What I Built

I maintain a few open source projects solo, and honestly, the hardest part isn’t writing code — it’s everything around it. Triaging issues, figuring out which ones are bugs vs feature requests, reading through the codebase to understand what’s broken, and then actually getting around to fixing it. Most issues sit there for weeks.

Ghost Maintainer is my attempt to fix that. It’s an AI-powered system that turns Notion into an operations center for your GitHub repository. When someone files an issue, it automatically:

  1. Lands in a Triage Queue in Notion
  2. Gets classified by Gemini as a bug or feature request (with confidence scoring)
  3. Routes to the right backlog — Maintenance Backlog for bugs, Feature Backlog for feature requests
  4. If it’s a bug (and you have auto-fix on), it reads your entire codebase, proposes a fix, and opens a PR

The only thing left for you is reviewing the PR and hitting merge.

For bug reports, if you have disabled auto PR creation, then you trigger a fix using:

ghost_maintainer fix 3

For features, you trigger implementation when you’re ready:

ghost_maintainer implement 9

That kicks off the same pipeline — reads the codebase, writes the code, opens a PR.

The whole thing installs with one command from inside your repo:

curl -sL https://raw.githubusercontent.com/sbis04/ghost_maintainer/main/install.sh 
  -o install.sh && 
  bash install.sh && 
  rm install.sh

It asks for your Notion, GitHub, and Gemini tokens, then sets up everything — databases, secrets, workflows, the lot.

I also built a full MCP server (in Dart, using the dart_mcp package) that exposes 5 tools, 2 resources, and 2 prompts. You can connect it to Gemini CLI or Claude and interact with your maintenance backlog conversationally — triage issues, investigate bugs, deploy fixes, all through natural language.

The Notion workspace

Ghost Maintainer Notion Page

  • Triage Queue — every issue starts here, AI sorts it
  • Maintenance Backlog — confirmed bugs with AI summaries, priority, and PR links
  • Feature Backlog — feature requests waiting for implementation
  • Archive — completed items with resolved dates
  • Project Vision Statement — you edit this to guide how the AI thinks about your project

Feature Request Notion Page

Video Demo

Link to Code on GitHub

GitHub logo

sbis04
/
ghost_maintainer

AI-powered junior partner for solo open-source maintainers

Ghost Maintainer

ghost maintainer cover

Solo maintainers wear too many hats. Ghost Maintainer takes over the repetitive parts — triaging issues, reading code, writing fixes, and opening PRs — so you can focus on the work that actually needs a human.

It uses Notion as an operations center. Bugs get triaged and fixed automatically. Features queue up until you’re ready. Everything stays visible in Notion so you never lose track.

Built with Dart, MCP, Google Gemini, and the Notion API.

Getting Started

You’ll need Dart (>= 3.7.0) and GitHub CLI installed.

Grab three API keys before you start:

What Where Notes
Notion token notion.so/profile/integrations See steps below
GitHub PAT github.com/settings/tokens Classic token, repo + actions scopes
Gemini key aistudio.google.com/apikey Any project works

You can either follow the steps in the next section, or have a look at the demo video to learn more about the tool and get step-by-step instruction for installing Ghost…

The project has three main pieces:

  • ghost_maintainer_mcp/ — The MCP server (Dart). 5 tools, 2 resources, 2 prompts.
  • notion_setup/ — The CLI tool and GitHub Action scripts that power the automation.
  • .github/workflows/ — 4 workflows: issue ingestion, triage, implementation, and PR archival.

The CLI doubles as a globally installable tool (dart pub global activate from the repo, no publishing needed).

How I Used Notion MCP

Notion is the central nervous system of Ghost Maintainer. Every piece of data flows through it.

As the Issue Tracker:
The Triage Queue, Maintenance Backlog, Feature Backlog, and Archive are all Notion databases. Each has structured properties (Stage, Priority, Labels, AI Summary, PR URL, Issue Number) that the automation reads and writes. When AI triages an issue, it updates the Notion page with its analysis. When a PR is created, the URL gets linked back. When the PR merges, the item moves to Archive with a timestamp.

As the AI’s Memory:
The Project Vision Statement page is a Notion document that the AI reads before every triage and investigation. It contains your project’s mission, principles, and current focus areas. This means the AI doesn’t just categorize issues mechanically — it understands that “security issues are always P0” or “we’re in a stability phase, deprioritize new features.” You edit a Notion page, and the AI’s behavior changes.

Through MCP:
The MCP server exposes Notion data as resources (ghost://vision for the vision statement, ghost://backlog/summary for live stats) and provides tools that read from and write to Notion. When you ask the MCP server to triage an issue, it pulls the vision from Notion, calls Gemini, then writes the results back to Notion. The protocol bridges conversational AI with structured project data.

The flow end to end:

GitHub Issue
    --> GitHub Action
    --> Notion Triage Queue
    --> AI Classification
    --> Notion Backlog
    --> AI Investigation
    --> GitHub PR
    --> Merge
    --> Notion Archive

Every step is visible in Notion. You always know where every issue is, what the AI decided, and why. No black boxes.

What I like most about this setup is that Notion becomes the source of truth that both humans and AI operate on. I check my Notion dashboard, I see what’s happening. The AI checks the same dashboard. We’re looking at the same board.

Full-Link Accountability for AI Agents

Core Event Primitives

Four standard event types (J, D, V, T) cover the full accountability lifecycle:

  • J: Judge – Create and initiate a judgment/decision

  • D: Delegate – Transfer authority or assign a task

  • V: Verify – Review and validate a record

  • T: Terminate – End a judgment or task lifecycle

Pain Points & Technical Solutions

Pain Point 1: Broken chain in multi-agent workflows, unable to trace root cause

Trigger Primitives: J + D

Solution: Add a task_based_on field to every record to enforce a hash reference to the parent task. A null value indicates the start of a chain; a populated value links to a preceding action, ensuring full end-to-end traceability.

Pain Point 2: Unclear accountability, hard to assign fault for errors

Trigger Primitives: D + V

Solution: Permanently include a who field in every record, bound to the actor’s DID or public key hash. Combined with cryptographic signing, records become non-repudiable and tamper-proof, enabling precise accountability.

Pain Point 3: Lack of compliant audit evidence for regulatory requirements

Trigger Primitives: V + T

Solution: Equip every record with a timestamp, unique nonce, and signature verification. Full audit trails with replay protection are natively supported, directly satisfying compliance requirements under the EU AI Act and Singapore IMDA frameworks.

Core Data Structure (Ready for Use)

{
“jep”: “1”,
“verb”: “J”,
“who”: “did:example:agent-789”,
“when”: 1742345678,
“what”: “122059e8878aa9a38f4d123456789abcdef01234”,
“nonce”: “f47ac10b-58cc-4372-a567-0e02b2c3d479”,
“aud”: “https://platform.example.com“,
“task_based_on”: “hash-of-parent-task”,
“ref”: “”,
“sig”: “eyJhbGciOiJFZERTQSJ9…”
}

Key Field Definitions

  • verb: Required; one of [J, D, V, T]

  • who: Required; unique identifier of the actor

  • when: Required; Unix timestamp to prevent stale/tampered records

  • nonce: Required; UUIDv4 to prevent replay attacks

  • task_based_on: Traceability field; hash of the parent task

  • ref: For verification events only; references the ID of the event being checked

  • sig: Required; JWS digital signature

Verification Logic (Pseudocode)

def verify_record(record):
# 1. Verify signature integrity
if not verify_jws_signature(record):
return “INVALID”
# 2. Ensure nonce uniqueness to prevent replay
if not is_valid_nonce(record[“nonce”]):
return “INVALID”
# 3. Validate timestamp within acceptable window
if not is_within_time_window(record[“when”]):
return “INVALID”
# 4. Verify parent task chain integrity
if record.get(“task_based_on”) and not task_exist(record[“task_based_on”]):
return “INVALID”
# 5. Verify events must include a reference
if record[“verb”] == “V” and not record.get(“ref”):
return “INVALID”
return “VALID”

Critical Security Rules

  • All records must be signed; any tampering invalidates the signature

  • Nonces must be globally unique; duplicate requests are rejected

  • Timestamp tolerance: ±5 minutes to account for clock skew

  • Verify (V) events must include a ref field to avoid circular validation

  • Ed25519 recommended; support for SM2, ECDSA P-256, and post-quantum algorithms

Optional Extensions

  • Task State: Add status field (pending, executing, completed, terminated)

  • Assignment Log: Record DID of assigner and assignee

  • Result Validation: Include confidence score and human review flag

  • Fault Handling: Log missing parent tasks and failure reasons for chain breaks

Implementation based on these IETF Drafts:

  • draft-wang-jep-judgment-event-protocol-01

  • draft-wang-jac-00

Building a Hate Speech Dataset with Responsible Web Scraping

Why Build Hate Speech Datasets?

AI moderation models are only as good as their training data. Researchers and companies building content moderation systems need labeled datasets of harmful content. Building these datasets responsibly requires careful ethical consideration and technical skill.

Ethical Framework First

Before writing any code, establish guidelines:

  • Purpose limitation — data used only for building detection models
  • Minimization — collect only what is needed for training
  • No amplification — never republish or redistribute raw hate speech
  • IRB approval — get institutional review board clearance for academic work
  • Secure storage — encrypt datasets, limit access

Architecture

Scraper -> Anonymizer -> Labeler -> Encrypted Storage

Setup

pip install requests beautifulsoup4 pandas cryptography

For accessing forums at scale, ScraperAPI handles proxy rotation and rate limiting.

The Responsible Scraper

import requests
from bs4 import BeautifulSoup
import pandas as pd
import hashlib
from datetime import datetime

SCRAPER_API_KEY = "YOUR_KEY"

def scrape_forum_posts(forum_url, max_pages=5):
    posts = []
    for page in range(1, max_pages + 1):
        url = f"http://api.scraperapi.com?api_key={SCRAPER_API_KEY}&url={forum_url}?page={page}"
        response = requests.get(url)
        soup = BeautifulSoup(response.text, "html.parser")

        for post in soup.select(".post, .comment, .message"):
            content = post.select_one(".content, .body")
            if content:
                posts.append({
                    "text": content.text.strip(),
                    "source_hash": hashlib.sha256(forum_url.encode()).hexdigest()[:12],
                    "scraped_at": datetime.now().isoformat()
                })
    return posts

Anonymization Pipeline

Critical step: remove all personally identifiable information before labeling.

import re

def anonymize_text(text):
    # Remove usernames, emails, URLs
    text = re.sub(r"@w+", "[USER]", text)
    text = re.sub(r"[w.+-]+@[w-]+.[w.]+", "[EMAIL]", text)
    text = re.sub(r"https?://S+", "[URL]", text)
    # Remove phone numbers
    text = re.sub(r"bd{3}[-.]?d{3}[-.]?d{4}b", "[PHONE]", text)
    # Remove names (basic NER approach)
    text = re.sub(r"b[A-Z][a-z]+ [A-Z][a-z]+b", "[NAME]", text)
    return text

def process_posts(raw_posts):
    processed = []
    for post in raw_posts:
        processed.append({
            "text": anonymize_text(post["text"]),
            "source_hash": post["source_hash"],
            "scraped_at": post["scraped_at"],
            "anonymized": True
        })
    return processed

Labeling Framework

LABEL_SCHEMA = {
    0: "clean",
    1: "offensive_language",
    2: "hate_speech",
    3: "threat"
}

def create_labeling_batch(posts, batch_size=50):
    df = pd.DataFrame(posts)
    df["label"] = None
    df["annotator"] = None
    df["confidence"] = None

    batches = [df[i:i+batch_size] for i in range(0, len(df), batch_size)]

    for i, batch in enumerate(batches):
        batch.to_csv(f"batch_{i:03d}.csv", index=False)
        print(f"Created batch_{i:03d}.csv with {len(batch)} samples")

    return len(batches)

Encrypted Storage

from cryptography.fernet import Fernet
import json

def encrypt_dataset(data, key_file="dataset.key"):
    key = Fernet.generate_key()
    with open(key_file, "wb") as f:
        f.write(key)

    cipher = Fernet(key)
    json_data = json.dumps(data).encode()
    encrypted = cipher.encrypt(json_data)

    with open("dataset.enc", "wb") as f:
        f.write(encrypted)

    print(f"Dataset encrypted. Key saved to {key_file}")
    print("Store the key separately from the dataset!")

def decrypt_dataset(key_file="dataset.key"):
    with open(key_file, "rb") as f:
        key = f.read()
    cipher = Fernet(key)
    with open("dataset.enc", "rb") as f:
        encrypted = f.read()
    return json.loads(cipher.decrypt(encrypted))

Quality Metrics

def calculate_inter_annotator_agreement(labels_a, labels_b):
    agreement = sum(a == b for a, b in zip(labels_a, labels_b))
    return agreement / len(labels_a)

def dataset_statistics(df):
    print(f"Total samples: {len(df)}")
    print(f"Label distribution:")
    for code, name in LABEL_SCHEMA.items():
        count = (df["label"] == code).sum()
        print(f"  {name}: {count} ({count/len(df)*100:.1f}%)")

Handling Protected Sources

Some forums require authentication or have aggressive bot detection. Use ThorData residential proxies for access, and ScrapeOps to monitor success rates.

Best Practices

  1. Never scrape private conversations or DMs
  2. Get ethics board approval for academic research
  3. Use content warnings when sharing results
  4. Balance your dataset to avoid bias
  5. Document your methodology thoroughly

Conclusion

Building hate speech datasets is essential for AI safety but must be done responsibly. With tools like ScraperAPI, proper anonymization, and encrypted storage, you can create valuable training data while respecting privacy and ethics.

“Per-Process Controls” (Quantized Executions)

Terms like “per-process controls” are understated in the programming world, and “quantized execution” is the key that makes these terms make sense.

I made a threading architecture that embodies “quantized execution” and “per-process controls” and learned that the key to a productive codebase is having a “surface of execution” that speaks for itself. Each operation is quantized to a unit of arguments (token) and each unit represents a flowing surface of metadata and heuristics.

The architecture I made TokenGate is described best as “an async threading hybrid model with per-process controls”.

In TokenGate there’s two layers of task authority, the first layer is the CPU and IO process tags to control affinity, the second is a direct admin control panel operated on the localhost of the rig running TokenGate.

Example of the control mechanisms in the GUI

Having (per-task or) per-operation commands to change the runtime is helpful, undoubtedly. What I want to discuss is if people would get on board with this if it was simple and practical?

This could be done in a variety of ways and is a huge enhancement to any code base.

As an example; if you had an analysis algorithm for tasks and a flag system, then you could act on this.

In whatever way (there’s plenty) a codebase could add controls to 100% of their systems critical operations. They could monitor their successful returns, predict memory needs, stop processes which are failing, and retain “control” by having a functional “surface of truth”.

This is possible when you “quantize an execution” or when functions are wrapped and decorated as tokens or if you go savvy with some other dynamic means of analysis and task manipulation. Point is, a “unit of execution” that has a “surface of truth” is much more “programmable” than a series of locked and time dependent operations using generic returns/state.

This doesn’t mean all functions are meant to have “controls” but anything which alters or returns data in this perspective should.

Am I the only one who thinks “task controls and analysis” is something that should be more of a conversation?

On a deeper level is “process analysis/mapping” and “ML integrated process handling” topics anyone else is intrigued by?

Also who would be on board with a new paradigm of task execution and analysis like this?

Anthropic Proved AI Can’t Evaluate Its Own Work. Here’s How I Rebuilt My Claude Code Setup Around That.

I’ve been building products with Claude Code for months. Every time I asked “is this implementation correct?”, the answer was “yes, it’s properly implemented.” Every time. Even when the code had bugs that broke in production.

Then Anthropic published a blog post that explained exactly why. I mapped my setup against their findings, and realized: my evaluator layer was almost empty.

Here’s how I rebuilt it.

Jump to:

  • What Anthropic’s experiment showed
  • Mapping this to Claude Code
  • Layer 1: Rules — always-on review criteria
  • Layer 2: Skills — on-demand reviewers
  • Layer 3: Agent separation — who builds vs who reviews
  • 3 principles for evaluation design
  • Final file structure
  • Harness design checklist

What Anthropic’s experiment showed

In March 2026, Anthropic published “Harness design for long-running apps” — experiments where AI agents autonomously built apps over multi-hour sessions.

The headline finding:

Agents asked to evaluate their own work tend to confidently praise it, even when it’s clearly mediocre to human observers.

It gets worse. Agents would spot real problems, then wave them off as unimportant and approve anyway. They’d skim instead of testing edge cases. Anthropic themselves put it bluntly: “Claude is an inadequate QA agent out of the box.”

Their fix was splitting generation from evaluation — three agents:

Agent Role
Planner Expands a one-line prompt into a full product spec
Generator Writes the code
Evaluator Clicks through the running app, finds bugs, scores against criteria

The difference was stark:

Setup Time Cost Result
Solo (no evaluator) 20 min $9 Core features broken
Full harness (with evaluator) 6 hrs $200 Basic functionality worked + AI features

The evaluator checked 27 criteria per sprint and filed bug reports like “fillRectangle exists but doesn’t fire on mouseUp.”

Reading this, it clicked: Claude Code’s config system can give you the same split.

Mapping to Claude Code

Here’s the mapping:

Anthropic’s agent Claude Code equivalent What goes here
Planner CLAUDE.md + planning skills Project context, constraints, design rationale
Generator Claude Code + technical skills Code generation
Evaluator Review agents + rules + hooks Quality gates, automated checks

When I laid out my own setup this way, the gap was obvious. CLAUDE.md had project context. Skills had coding patterns. But nothing was actually checking whether the output was correct. I was asking the generator to review its own work — the exact failure mode Anthropic documented.

Layer 1: Rules — always-on review criteria

Files in ~/.claude/rules/ load every session. Put things here that AI won’t do on its own but that matter in production.

Here’s a taste of my Supabase/PostgreSQL rules (30 total):

# ~/.claude/rules/supabase-postgres.md

## Index FK columns
PostgreSQL does NOT auto-index foreign key columns.

## RLS performance
Wrap auth.uid() in SELECT to prevent per-row execution:
- BAD: using (user_id = auth.uid())
- GOOD: using (user_id = (select auth.uid()))

## Cursor-based pagination
- BAD: OFFSET (slow on deep pages)
- GOOD: WHERE id > $last_id ORDER BY id LIMIT 20

Without this rule, the AI generates auth.uid() without the SELECT wrapper every time. Works fine with small tables. Tests pass. Then production slows to a crawl as rows grow. Classic “surface-level test that misses the deeper bug” — exactly what Anthropic described.

I think of rules as “never step on this mine again” files. Every rule started as a real mistake.

Layer 2: Skills — on-demand reviewers

Rules load every session, so too many will eat your context window. Anything task-specific goes into skills (.claude/skills/), which show only their titles by default and load on demand.

You can wire up keyword-triggered activation:

# ~/.claude/CLAUDE.md (excerpt)

## Automatic Skill Activation

### Testing / TDD
**Trigger:** test, TDD, coverage, unit test
**Action:** Run test-driven-development skill

### Bug / Error handling
**Trigger:** bug, error, debug, broken
**Action:** Run systematic-debugging skill

### Completion check
**Trigger:** done, verify, review, check
**Action:** Run verification-before-completion skill

The completion check is the big one. In Anthropic’s experiment, the evaluator ran checks at the end of each sprint. Same idea here — say “verify this” and a quality checklist kicks in behind the scenes.

I run 27 skills total. 7 auto-activate on keywords.

Layer 3: Agent separation — who builds vs who reviews

The core insight from Anthropic: separate the builder from the reviewer. In Claude Code, you do this through agent orchestration:

# ~/.claude/rules/agents.md

## Role: Manager
You are a manager and agent orchestrator.

**Rules:**
- Never implement directly — delegate all implementation to Sub Agents
- Break tasks into small units and run PDCA cycles

## Delegation
### Always delegate:
1. Code implementation
2. Debugging
3. Test creation

### Manager handles directly:
1. Task decomposition
2. Progress verification
3. Plan adjustment

The trick: pin the main Claude as “manager = reviewer” and push all implementation to Sub Agents. This gives Claude Code the same generator/evaluator split that Anthropic proved works.

Main Claude doesn’t write code. It reviews what Sub Agents produce. Planner + evaluator = main Claude. Generator = Sub Agents.

3 principles for evaluation design

1. Weight your criteria toward what AI misses

Anthropic’s frontend experiment used 4 criteria: design quality, originality, technical execution, functionality. They weighted design and originality higher — because the AI already did well on technical execution and functionality.

Lower the weight on things AI handles naturally. Raise it on things AI overlooks.

In practice:

  • Low weight (AI’s fine): syntactically correct code, basic API endpoints
  • High weight (AI misses): performance traps (auth.uid() without SELECT), UX decisions that affect conversion, security blind spots

2. Prune your harness as models improve

From Anthropic:

Every component of a harness encodes an assumption about what the model can’t do alone, and these assumptions can be wrong or quickly become outdated as models improve.

When they went from Opus 4.5 to 4.6, sprint decomposition became unnecessary — the newer model handled long sessions on its own. Same thing happens with your Claude Code rules. Something essential today may be dead weight next quarter.

I’ve revised my CLAUDE.md 8 times. The test for each line: “If I delete this, will the AI make mistakes?” No? Cut it.

3. Better models mean more harness possibilities, not fewer

The space of interesting harness combinations expands rather than contracts as models improve.

Sounds backwards, but it matches my experience. As the AI gets smarter, you can delegate more. But the remaining evaluation points get more nuanced, more subtle, and higher-stakes. The easy rules go away. The hard judgment calls stay.

Final file structure

~/.claude/
├── CLAUDE.md                # Planner layer (project overview + skill triggers)
├── rules/
│   ├── agents.md            # Generator/evaluator split
│   ├── supabase-postgres.md # Review criteria (DB/RLS, 30 rules)
│   ├── react-nextjs.md      # Review criteria (React/Next.js)
│   ├── security.md          # Review criteria (security)
│   ├── coding-style.md      # Code quality
│   └── testing.md           # Test quality
└── skills/                   # On-demand reviewers
    ├── verification-before-completion  # End-of-task checks
    ├── systematic-debugging            # Debug-time checks
    ├── test-driven-development         # TDD enforcement
    └── ... (27 total)

How it maps to Anthropic’s architecture:

  • CLAUDE.md → Planner artifact (the product spec)
  • rules/ → Review criteria (the sprint contract)
  • skills/ → Specialist reviewers (activate when needed)
  • agents.md → Builder/reviewer separation

Harness design checklist

Planner layer (CLAUDE.md)

  • [ ] Project overview and constraints documented
  • [ ] Design decisions and their rationale included
  • [ ] Under 200 lines (bloated CLAUDE.md degrades instruction-following)

Review criteria (rules/)

  • [ ] Cover what AI misses, not what it already does well
  • [ ] Weighted toward production-critical concerns
  • [ ] Regularly pruned as models improve

On-demand reviewers (skills/)

  • [ ] Separated by domain (DB, React, security, etc.)
  • [ ] Keyword auto-activation configured
  • [ ] Completion verification skill exists

Builder/reviewer separation (agents.md)

  • [ ] Main Claude delegates implementation to Sub Agents
  • [ ] Verification flow for implementation results

Links

Pointers in C — Complete Guide

Pointers are one of the most powerful and fundamental features in the C programming language. If you truly understand pointers, you unlock low-level memory control, efficient data structures, and system-level programming.

1. What is a Pointer?

A pointer is a variable that stores the memory address of another variable.

int a = 10;
int *p = &a;

Memory Diagram

Stack Memory:

+------------+        +------------+
| a = 10     | 0x100  | p = 0x100  | 0x200
+------------+        +------------+

p ───────────────► a

Debug Scenario

  • ❌ Bug: printing pointer incorrectly
printf("%d", p); // WRONG
  • ✅ Fix:
printf("%p", (void*)p);

2. Pointer Declaration

data_type *pointer_name;

Memory Insight

The pointer size is fixed (typically 4 or 8 bytes), regardless of type.

int *p;   // 8 bytes (on 64-bit system)
char *c;  // 8 bytes

Debug Scenario

  • ❌ Misinterpretation bug
char *p;
int x = 1000;
p = (char*)&x;
printf("%d", *p); // unexpected value

3. Address Operator (&)

int x = 5;
printf("%p", &x);

Memory Diagram

+------------+
| x = 5      | 0x300
+------------+

&x = 0x300

4. Dereference Operator (*)

int x = 5;
int *p = &x;
printf("%d", *p);

Memory Flow

p (0x400) ───► 0x300 ───► value 5

Debug Scenario (Segmentation Fault)

int *p = NULL;
printf("%d", *p); // CRASH

✔ Cause: dereferencing NULL

5. Pointer Initialization

Memory Risk

int *p;  // contains garbage address like 0xDEADBEEF

Debug Scenario (Crash)

int *p;
*p = 10; // undefined behavior

✔ Fix:

int *p = NULL;

6. NULL Pointer

int *p = NULL;

Memory Representation

p = 0x0

Safe Usage Pattern

if (p != NULL) {
    *p = 10;
}

7. Pointer Arithmetic

int arr[3] = {10, 20, 30};
int *p = arr;
p++;

Memory Diagram

Address:   0x100   0x104   0x108
Values:    10      20      30
             ▲
             p (after p++)

Debug Scenario (Out of Bounds)

int arr[3] = {1,2,3};
int *p = arr;
p += 5;
printf("%d", *p); // undefined

8. Pointers and Arrays

int arr[3] = {1,2,3};
int *p = arr;

Internal Model

arr == &arr[0]
p + 1 == &arr[1]

Debug Scenario

printf("%lu", sizeof(arr)); // 12
printf("%lu", sizeof(p));   // 8

✔ Arrays ≠ pointers

9. Pointer to Pointer

int x = 5;
int *p = &x;
int **pp = &p;

Memory Diagram

pp ──► p ──► x
0x500  0x400  0x300

10. Function Pointers

int add(int a, int b) { return a + b; }
int (*fp)(int,int) = add;

Memory Model

Code Segment:
add() at 0x900

Stack:
fp = 0x900

Debug Scenario

int (*fp)(int,int);
fp(2,3); // crash

✔ Fix: initialize before use

11. Dynamic Memory Allocation

int *p = malloc(sizeof(int));
*p = 10;
free(p);

Memory Diagram

Heap:
0xA00 → [10]

Stack:
p = 0xA00

Debug Scenario (Memory Leak)

int *p = malloc(sizeof(int));
p = NULL; // lost reference

✔ Leak: memory still allocated

12. Void Pointer

void *p;

Rule

Cannot dereference without casting

int x = 5;
p = &x;
printf("%d", *(int*)p);

13. Dangling Pointer

int *p = malloc(sizeof(int));
free(p);

Memory Diagram

p ──► freed memory (invalid)

Debug Scenario

*p = 20; // undefined behavior

✔ Fix:

free(p);
p = NULL;

14. Wild Pointer

int *p;

Memory

p = random address

Debug Scenario

*p = 10; // crash

15. Const Pointer

const int *p;
int *const p;

Memory Behavior

  • const int *p → value locked
  • int *const p → address locked

16. Pointer and Strings

char *str = "Hello";

Memory Layout

Code Segment:
"Hello" (read-only)

Stack:
str → address of string

Debug Scenario

str[0] = 'h'; // crash (read-only memory)

17. Pointers vs Arrays

Deep Difference

int arr[3]; // fixed memory
int *p;     // dynamic reference

Debug Scenario

arr = p; // ❌ illegal

18. Memory Layout (Deep View)

+-------------------+
| Code Segment      |
+-------------------+
| Global/Static     |
+-------------------+
| Stack             |
|  - local vars     |
+-------------------+
| Heap              |
|  - malloc memory  |
+-------------------+

Debug Scenario (Stack Overflow)

void func() {
    int arr[1000000];
}

19. Common Mistakes (With Debug Insight)

  • Uninitialized pointer → crash
  • Double free → heap corruption
  • Memory leak → performance issue
  • Out-of-bounds → undefined behavior

20. Best Practices (Professional Level)

  • Always initialize pointers
  • Use NULL after free
  • Use tools like:

    • valgrind
    • address sanitizer

🚀 5 Practical Projects Using Pointers (Advanced Level)

1. Dynamic Array (Vector)

Add:

  • resizing logic
  • capacity vs size tracking

2. Linked List (Singly)

Add:

  • insert/delete at position
  • reverse list (pointer manipulation heavy)

3. String Library (Pointer-only)

Implement using only pointers (no indexing)

4. Custom Memory Allocator

Simulate heap blocks:

[metadata][data][metadata][data]

5. Callback/Event System

Simulate event handlers using function pointers

Conclusion

Pointers are not just a feature — they are the core of C programming.

With memory diagrams + debugging understanding, you now think like a systems programmer.

What was your win this week!?

From the film Ferris Bueller

👋👋👋👋

Looking back on your week — what was something you’re proud of?

All wins count — big or small 🎉

Examples of ‘wins’ include:

  • Getting a promotion!
  • Starting a new project
  • Fixing a tricky bug
  • Taking a well deserved day off! 💤💤💤

From the film Ferris Bueller's Day Off: three friends ride in a red convertible with the top down on a sunny day.

Happy Friday!

Why bcrypt Is Not Enough in 2026 And What We Built Instead

The Story Behind This Package

Every time I started a new Node.js backend project, I found myself doing the same thing.

npm install bcrypt
npm install joi
# copy-paste AppError class from some tutorial

Three packages. Three different docs. Three different ways of doing the same things. And I did this in every single project — and so did every developer I knew.

Me and my friend Ubed decided to fix this. We built nodearmor —a single npm package that replaces all of that with one install.

npm install nodearmor

This is the story of why we built it, what problem each module solves, and what we learned publishing our first open source npm package.

The Problem With bcrypt in 2026

Let me start with the biggest one — password hashing.

bcrypt has been the default answer to “how do I hash passwords in Node.js” for over 15 years. Search that question right now. bcrypt is in the first three results. Every tutorial uses it. Every project copy-pastes it.

Here is the problem nobody talks about. bcrypt was designed in 1999.

In 1999, hashing was done on CPUs. CPUs had one or two cores. An attacker trying to crack a stolen database of bcrypt hashes could try maybe 100 passwords per second. At that speed, cracking a strong password would take years.

In 2025, attackers use GPUs. A modern GPU has 4,000 to 10,000 cores. Every core can attempt a bcrypt hash simultaneously. That 100 attempts per second becomes 400,000 attempts per second. The slowness that bcrypt provides is completely parallelized away.

bcrypt is not broken. But it is not the best we can do in 2025.

The Solution — Argon2id

Argon2id won the international Password Hashing Competition in 2015 — a competition run by cryptography researchers specifically to find the best password hashing algorithm for the modern era.

The key difference is that Argon2id is memory-hard.

Every single hash attempt requires a large, fixed amount of RAM — 64MB by default. You cannot skip the RAM requirement. The algorithm is mathematically designed so that without the full 64MB, the computation cannot complete.

Here is the math that matters:

bcrypt attack in 2025:
GPU with 4,000 cores — 400,000 parallel attacks per second

Argon2id attack in 2025:
GPU with 16,000 MB of RAM
16,000 MB ÷ 64 MB per attempt = 250 parallel attacks maximum

The attacker with Argon2id needs 16 times more GPUs to achieve the same attack speed they had with bcrypt. GPUs cost money. Electricity costs money. The attack becomes economically impractical.

OWASP — the Open Web Application Security Project — updated their recommendation in 2025. Argon2id is their first choice for password hashing. bcrypt is listed as acceptable only if Argon2id is not available.

What nodearmor Does

nodearmor bundles four modules that every serious Node.js backend needs:

Module Replaces What It Does
nodearmor/env dotenv + manual checks Type-safe env validation at startup
nodearmor/hash bcrypt Argon2id password hashing
nodearmor/guard joi / express-validator Zod-powered request validation
nodearmor/errors copy-pasted AppError Typed HTTP error classes

Let me walk through each one.

Module 1 — env

The Problem

What happens when your app starts without a DATABASE_URL?

It starts fine. No errors at boot time. Then 10 minutes later, when it tries to connect to the database, it crashes — with an error that points to a database file, not the missing environment variable. You spend 20 minutes debugging something that should have been caught immediately.

The Solution

nodearmor/env validates every environment variable at startup. If anything is missing or wrong, the process exits immediately before the server starts — with a clear message listing every problem at once.

import { envault } from "nodearmor/env";

export const env = envault({
  DATABASE_URL: { type: "string", message: "Set DATABASE_URL to your PostgreSQL connection string" },
  PORT:         { type: "number", default: 3000, min: 1000, max: 65535 },
  DEBUG:        { type: "boolean", default: false },
  NODE_ENV:     { type: "string", enum: ["development", "production", "test"] },
  API_BASE_URL: { type: "url" },
  ADMIN_EMAIL:  { type: "email" },
  STRIPE_KEY:   { type: "string", required: false },
});

// All values are correctly typed — no casting needed anywhere
env.PORT        // number
env.DEBUG       // boolean
env.NODE_ENV    // string
env.STRIPE_KEY  // string | undefined

When validation fails, you see this — all problems at once:

nodearmor/env — validation failed:

  x  Missing required variable: "DATABASE_URL"
  x  "PORT" must be >= 1000, got: 80
  x  "NODE_ENV" must be one of [development, production, test], got: "staging"

Fix everything. Restart once. No more guessing.

What We Overcame

Before envault, developers wrote this in every project:

if (!process.env.DATABASE_URL) {
  console.error("DATABASE_URL is required");
  process.exit(1);
}

const PORT = parseInt(process.env.PORT || "3000");
if (isNaN(PORT)) {
  console.error("PORT must be a number");
  process.exit(1);
}

15 lines of boilerplate that grows with every new variable. No TypeScript types. No defaults. No format validation. envault replaces all of it with a clean schema definition.

Module 2 — hash

The Problem

bcrypt — already covered above. CPU-only, 1999, vulnerable to modern GPU attacks.

The Solution

nodearmor/hash wraps Argon2id with OWASP 2025 recommended defaults baked in. The API is almost identical to bcrypt.

import { hash, verify, needsRehash } from "nodearmor/hash";

// REGISTER — hash before storing in database
const passwordHash = await hash(plainPassword);
// Returns: "$argon2id$v=19$m=65536,t=3,p=1$..."

// LOGIN — verify against stored hash
const isValid = await verify(passwordHash, plainPassword);
// Returns: true or false

// MAINTENANCE — upgrade old hashes automatically
if (await needsRehash(passwordHash)) {
  const newHash = await hash(plainPassword);
  await db.users.updateHash(userId, newHash);
}

Notice the hash string that comes back — $argon2id$v=19$m=65536,t=3,p=1$.... It is self-describing. The algorithm name, version, all parameters, and the random salt are all embedded in the string. You never need to store the salt separately. You never need to remember what parameters you used.

Migrating From bcrypt

You do not need to force users to reset their passwords. The migration is transparent — users upgrade automatically on their next successful login.

import bcrypt from "bcrypt";
import { hash, verify } from "nodearmor/hash";

async function login(email: string, plainPassword: string) {
  const user = await db.users.findByEmail(email);

  let isValid = false;

  if (user.passwordHash.startsWith("$2b$")) {
    // old bcrypt hash — verify with bcrypt
    isValid = await bcrypt.compare(plainPassword, user.passwordHash);
    if (isValid) {
      // silently upgrade to Argon2id on next login
      await db.users.updateHash(user.id, await hash(plainPassword));
    }
  } else {
    // new Argon2id hash
    isValid = await verify(user.passwordHash, plainPassword);
  }

  if (!isValid) throw new Unauthorized("Invalid credentials");
  return issueToken(user);
}

Module 3 — guard

The Problem

Without validation middleware, every developer writes this in every route:

app.post("/register", async (req, res) => {
  if (!req.body.email || !req.body.email.includes("@")) {
    return res.status(400).json({ error: "Invalid email" });
  }
  if (!req.body.password || req.body.password.length < 8) {
    return res.status(400).json({ error: "Password too short" });
  }
  // ... 10 more lines of manual checks
  // ... finally the actual route logic
});

10 to 15 lines of boilerplate. No TypeScript types on req.body. Copy-pasted into every route. Different format in every project. Frontend developers do not know what error shape to expect.

The Solution

nodearmor/guard takes a Zod schema and returns an Express middleware. One line. Validation runs before your route handler. req.body is fully typed.

import { guard } from "nodearmor/guard";
import { z }     from "nodearmor";

const RegisterSchema = z.object({
  email:    z.string().email(),
  password: z.string().min(8),
  name:     z.string().min(1).max(100),
});

app.post("/register",
  guard(RegisterSchema),       // one line — validation done
  async (req, res) => {
    const { email, password, name } = req.body; // fully typed
    // no validation code here — it already ran
  }
);

When validation fails, the response is always the same structure:

{
  "status": 400,
  "code": "VALIDATION_FAILED",
  "message": "Request validation failed",
  "issues": [
    { "field": "email",    "message": "Invalid email",     "code": "invalid_string" },
    { "field": "password", "message": "String must contain at least 8 character(s)", "code": "too_small" }
  ]
}

Same shape. Every route. Every project. Your frontend writes one error handler and it works everywhere.

You can also validate query parameters and URL parameters:

// Validate query params — GET /users?page=2&limit=50
const PaginationSchema = z.object({
  page:  z.coerce.number().min(1).default(1),
  limit: z.coerce.number().min(1).max(100).default(20),
});

app.get("/users", guard(PaginationSchema, "query"), async (req, res) => {
  const { page, limit } = req.query; // real numbers, not strings
});

// Validate URL params — GET /users/:id
const IdSchema = z.object({
  id: z.string().uuid("User ID must be a valid UUID"),
});

app.get("/users/:id", guard(IdSchema, "params"), async (req, res) => {
  const { id } = req.params; // guaranteed to be a valid UUID
});

Module 4 — errors

The Problem

Search any Node.js project on GitHub. You will find one of these patterns:

// Pattern 1 — magic numbers everywhere
return res.status(404).json({ error: "User not found" });

// Pattern 2 — copy-pasted AppError class
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode; // sometimes 'status', sometimes 'statusCode'
  }
}

// Pattern 3 — everyone writes it differently
throw new AppError("User not found", 404); // no code field, no meta, no types

Every project looks different. Frontend developers write defensive code trying to handle every possible error shape.

The Solution

nodearmor/errors provides typed HTTP error classes that always produce the same JSON structure.

import {
  NotFound, Conflict, Unauthorized,
  Forbidden, TooManyRequests,
  isApiError, toResponse,
} from "nodearmor/errors";

// In your routes — one line, fully typed
throw new NotFound("User not found", { userId: req.params.id });
throw new Conflict("Email already registered", { field: "email" });
throw new Unauthorized("Token expired or invalid");
throw new Forbidden("Admin access required");
throw new TooManyRequests("Rate limit exceeded", { retryAfter: 60 });

Every error serializes to the same shape:

{
  "status":  409,
  "code":    "CONFLICT",
  "message": "Email already registered",
  "meta":    { "field": "email" }
}

Write one error handler for your entire app:

import { isApiError, toResponse } from "nodearmor/errors";

app.use((err, req, res, next) => {
  if (isApiError(err)) {
    return res.status(err.status).json(toResponse(err));
  }

  // unknown error — log internally, hide details from client
  // stack trace is never sent to clients — security best practice
  console.error("[UNHANDLED ERROR]", err);
  res.status(500).json({
    status:  500,
    code:    "INTERNAL_SERVER_ERROR",
    message: "Something went wrong. Please try again.",
  });
});

A Complete Working Example

Here is a full register and login flow using all four modules:

// src/env.ts
import { envault } from "nodearmor/env";

export const env = envault({
  DATABASE_URL: { type: "string" },
  PORT:         { type: "number", default: 3000 },
  JWT_SECRET:   { type: "string" },
  NODE_ENV:     { type: "string", enum: ["development", "production", "test"] },
});


// src/routes/auth.ts
import { Router }                 from "express";
import { z }                      from "nodearmor";
import { hash, verify }           from "nodearmor/hash";
import { guard }                  from "nodearmor/guard";
import { Conflict, Unauthorized } from "nodearmor/errors";

const router = Router();

const RegisterSchema = z.object({
  email:    z.string().email(),
  password: z.string().min(8),
  name:     z.string().min(1),
});

const LoginSchema = z.object({
  email:    z.string().email(),
  password: z.string().min(1),
});

router.post("/register", guard(RegisterSchema), async (req, res) => {
  const { email, password, name } = req.body;

  const existing = await db.users.findByEmail(email);
  if (existing) throw new Conflict("Email already registered", { field: "email" });

  const passwordHash = await hash(password);
  const user = await db.users.create({ email, name, password: passwordHash });

  res.status(201).json({ id: user.id, email: user.email });
});

router.post("/login", guard(LoginSchema), async (req, res) => {
  const { email, password } = req.body;

  const user = await db.users.findByEmail(email);
  if (!user) throw new Unauthorized("Invalid email or password");

  const isValid = await verify(user.passwordHash, password);
  if (!isValid) throw new Unauthorized("Invalid email or password");

  const token = jwt.sign({ userId: user.id }, env.JWT_SECRET);
  res.json({ token });
});

export default router;


// src/app.ts
import express          from "express";
import { env }          from "./env";
import { errorHandler } from "./middleware/errorHandler";
import authRoutes       from "./routes/auth";

const app = express();
app.use(express.json());
app.use("/auth", authRoutes);
app.use(errorHandler);

app.listen(env.PORT, () => {
  console.log(`[${env.NODE_ENV}] Server running on port ${env.PORT}`);
});

Why One Package Instead of Three

bcrypt + joi + AppError nodearmor
Install Three separate commands npm install nodearmor
Extra dependencies Manual peer dep installs Everything included
Versions to track Three changelogs One
TypeScript support Mixed — some need @types Native throughout
Password algorithm bcrypt — 1999, CPU-only Argon2id — 2025, memory-hard
Error response shape Different in every project Always status + code + message + meta
Validation boilerplate 10-15 lines per route One line per route

Security

nodearmor v1.0.5 passes npm audit with zero vulnerabilities.

Argon2id default parameters follow OWASP 2025 recommendations:

  • Memory cost: 64 MB per hash attempt

  • Time cost: 3 iterations

  • Parallelism: 1 thread

Stack traces from your server are never included in HTTP error responses.

How to Get Started

npm install nodearmor

That is it. All dependencies are included automatically. No peer dependency installs needed.

Full documentation and source code:

If you find a bug or want to contribute, open an issue or pull request on GitHub. We review everything.

Final Thought

bcrypt still works. We are not saying it is broken. We are saying that in 2025, with GPU attacks being as cheap and accessible as they are, there is a better option that takes the same amount of code to use.

Argon2id is that option. nodearmor makes it the default.

One install. Four protections. Zero excuses for insecure backends.

Built by Shaswat and Ubed- real open source package.

If this helped you, share it with a developer who is still using bcrypt.