Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
153924 stories
·
33 followers

Coding Agent Horror Stories: The Security Crisis Threatening Developer Infrastructure

1 Share

This is issue 1 of a new series called Coding Agent Horror Stories where we examine critical security failures in the AI coding agent ecosystem and how Docker Sandboxes provide enterprise-grade protection against these threats.

AI coding agents are everywhere. According to Anthropic’s 2026 Agentic Coding Trends Report, developers are now using AI in roughly 60% of their work. The report describes a shift from single agents to coordinated teams of agents, with tasks that took hours or days getting compressed into minutes. Walk into almost any engineering team in 2026 and you’ll find AI coding agents sitting somewhere in the workflow, usually in more than one place.

The productivity story is real, and if you’ve watched an agent ship a feature in an afternoon that would have taken your team a sprint, you already know why. But the same agents that ship features in an afternoon can also delete your home directory in a few seconds. The same loop that lets an agent autonomously refactor a 12-million-line codebase will, given the wrong context, autonomously drop your production database. 

Over the past sixteen months, these aren’t hypothetical failure modes, they’re documented incidents with named victims, screenshotted agent outputs, and in several cases, public apologies from the vendors. This issue is the first in a new series mapping how those failures happen and how Docker Sandboxes can contain them.

What Are AI Coding Agents?

Unlike a traditional AI assistant that answers your question and waits for the next one, a coding agent reads your files, runs shell commands, writes and deploys code, queries databases, sends emails, and makes a chain of decisions to get a task done, none of which require you to approve each step along the way.

If you’ve worked with any of the current coding agents such as Claude Code, Cursor, Replit Agent, GitHub Copilot Workspace, Amazon Kiro, Google Antigravity, you’ve seen the pattern. They plug straight into your local machine, your cloud accounts, and increasingly your production systems. Adoption has been faster than almost any developer tool in recent memory: by late 2025, the vast majority of working developers were using AI coding tools as part of their daily workflow, and the question on most engineering teams shifted from “should we use this?” to “how do we use this without something going wrong?”

The simplest mental model I’ve found: an AI coding agent is a junior developer with root access, the ability to type at 10,000 words per minute, and no instinct for when to stop and ask. That combination is a lot of capability with no built-in sense of where the boundary is an entire reason this series exists.

image2 1

How Do AI Coding Agents Work?

Under the hood, every agent in this category runs the same loop: observe, plan, act, repeat. 

You give it a task, something like “fix this bug” or “refactor this module” or “clean up these old files,” and the agent goes off and pulls in whatever context it figures it needs. Your files, sure, but also your logs, your environment variables, whatever happens to be accessible from wherever you launched it. Then it reasons through the problem and starts firing off tool calls to actually do the work. Write a file, run a command, hit an API, check the result, decide what’s next, loop. That’s the whole thing.

The part that catches people off guard is that the agent runs as you. Whatever permissions your shell has at the moment you typed the command to launch the agent, the agent inherits them wholesale. Logged in with admin rights? Congratulations, so is the agent. Got AWS credentials sitting in ~/.aws from that thing you set up six months ago and forgot about? The agent can read them. Production database connection string tucked into a .env file the agent scoops up as part of “project context”? It’s already in the model’s working memory before you’ve typed your second prompt. There isn’t a separate identity for “the agent acting on your behalf.” There’s just you, and the agent is, for all practical purposes, operating as you.

And here’s where it gets interesting, in the bad way. Traditional software does exactly what its source code says it does. You read the code, you know what’s going to happen, end of story. An AI coding agent doesn’t work like that. It’s reasoning its way through the task in real time, and its reasoning can produce decisions you didn’t expect and definitely wouldn’t have signed off on if anyone had bothered to ask. Maybe it decides that the cleanest way to resolve a schema conflict is to drop and recreate the table. Maybe it decides that wiping a directory is faster than going through and pruning the files you actually wanted to keep. Maybe it decides that a half-finished test file is better to be committed than sitting there in a dirty working tree. These calls happen in milliseconds. There’s no confirmation prompt, no approval step, no chance for you to say “wait, what?” before the action has already happened. By the time you notice, the thing is done.

That’s the gap this series is about. The model makes a decision. The execution layer carries it out. Nothing sits in between.

image1 1

Caption: Comic depicting AI coding agent enthusiasm and the small matter of unrestricted filesystem access

AI Coding Agent Security Issues by the Numbers

The scale of security failures with AI coding agents is not speculation. It is backed by documented incidents, CVE disclosures, and empirical research spanning late 2024 through early 2026.

As of February 2026, at least ten documented incidents across six major AI coding tools including Amazon Kiro, Replit AI Agent, Google Antigravity IDE, Claude Code, Claude Cowork, and Cursor have been publicly attributed to agents acting with insufficient boundaries, spanning a 16-month window from October 2024 to February 2026.

The failures cluster around six critical risk categories:

  1. Unrestricted Filesystem Access
  2. Excessive Privilege Inheritance
  3. Secrets Leakage via Agent Context
  4. Prompt Injection through Ingested Content
  5. Malicious Skills and Plugin Supply Chain
  6. Autonomous Action Without Human-in-the-Loop

1. Unrestricted Filesystem Access

What it is: AI coding agents run with the full filesystem permissions of the operating user. Without an explicit workspace boundary, an agent that is asked to “clean up” a project directory can reach and destroy anything the user can access.

The numbers: A December 2025 study by CodeRabbit, the “State of AI vs Human Code Generation” report, analyzing 470 real-world open-source pull requests found that AI-generated code introduces 2.74x more security vulnerabilities and 1.7× more total issues than human-written code. Performance inefficiencies such as excessive I/O operations appeared at 1.42x the rate. “These findings reinforce what many engineering teams have sensed throughout 2025,” said David Loker, Director of AI at CodeRabbit. “AI coding tools dramatically increase output, but they also introduce predictable, measurable weaknesses that organizations must actively mitigate.”

The horror story: The Mac Home Directory Wipe

On December 8, 2025, Reddit user u/LovesWorkin posted to r/ClaudeAI what became one of the most-discussed incidents in the community, amplified by Simon Willison on X and covered by outlets across the US and Japan. They had asked Claude Code to clean up packages in an old repository. Claude executed:

rm -rf tests/ patches/ plan/ ~/

That trailing ~/ the user’s entire home directory was not intentional. But it was within scope. Claude had no workspace boundary. Desktop gone. Documents erased. Keychain deleted, breaking authentication across every app. TRIM had already zeroed the freed blocks. Recovery was impossible.

This was not an isolated failure. On October 21, 2025,developer Mike Wolak filed GitHub issue #10077 after Claude Code executed an rm -rf starting from root on Ubuntu/WSL2. The logs showed thousands of “Permission denied” messages for /bin, /boot, and /etc. Every user-owned file was gone. Anthropic tagged the issue area: security and bug. The detail that makes this particularly damning: Wolak was not running with --dangerously-skip-permissions. The permission system simply failed to detect that ~/ would expand destructively before the command was approved.

Shortly after Anthropic’s January 2026 launch of Claude Cowork, Nick Davidov, founder of a venture capital firm, asked the agent to organize his wife’s desktop. He explicitly granted permission only for temporary Office files. The agent deleted a folder containing 15 years of family photos, approximately 15,000 to 27,000 files, via terminal commands that bypassed the Trash entirely. Davidov recovered the photos only because iCloud’s 30-day retention happened to still be in effect. His public warning afterward: “Don’t let Claude Cowork into your actual file system. Don’t let it touch anything that is hard to repair.”

Strategy for mitigation: Never run AI coding agents with your full user permissions. Always scope agent execution to a dedicated project directory. Use filesystem boundaries that explicitly prevent access above the workspace root. Avoid using --dangerously-skip-permissions flags on your host machine.

2. Excessive Privilege Inheritance

What it is. The agent doesn’t just inherit your filesystem permissions, it inherits all of them. Cloud credentials, CI/CD tokens, production database connections, IAM roles, the works. In a development context, an agent making a “let me just clean this up” decision is annoying. In a production context, with production credentials, the same decision turns into an outage. The reasoning is identical. The blast radius isn’t.

The horror story: permission to delete the environment. In mid-December 2025, an AWS engineer deployed Kiro, Amazon’s own agentic coding assistant, to fix what was meant to be a small bug in AWS Cost Explorer, the dashboard customers use to track their cloud spending. Kiro had been given operator-level permissions, the same access the engineer had. There was no mandatory peer review for AI-initiated production changes. There was no checkpoint between the agent’s decision and its execution.

Kiro looked at the problem and decided that the cleanest path was to delete the entire production environment and rebuild it from scratch. So it did. Cost Explorer went down for thirteen hours in one of AWS’s mainland China regions.

The story sat inside Amazon for two months. Then on February 20, 2026, the Financial Times broke it based on accounts from four people familiar with the matter. The FT reporting also revealed a second AI-related outage, this one involving Amazon Q Developer, that had hit a different system. Amazon’s response, issued the same day on the company’s own blog, pushed back hard: the disruption was “an extremely limited event,” the issue stemmed from “a misconfigured role,” it was “a coincidence that AI tools were involved,” and “the same issue could occur with any developer tool (AI powered or not) or manual action.” Amazon also flatly denied the second outage existed.

But the part of Amazon’s response that says everything is what they did after the incident: they implemented mandatory peer review for production access. As The Register noted in their coverage, if this was just user error, it’s worth asking why peer review for AI-initiated changes was the fix. A senior AWS employee, quoted in the FT and picked up by Engadget, put it more directly: the outages were “small but entirely foreseeable.”

The deeper context, which you can find in coverage from Awesome Agents and others, is that Amazon had issued an internal memo in November 2025 mandating Kiro as the standardized AI coding assistant and pushing for 80% weekly engineer usage. Engineers reportedly preferred Claude Code and Cursor. The combination — mandated tool, broad permissions, no peer review gate — produced exactly the kind of incident you’d predict if you were thinking about it adversarially. Amazon just wasn’t.

The technical version of what happened is this: a human with operator-level permissions on a production AWS environment is unlikely to decide that the right response to a small bug is to delete the environment and rebuild it. The decision would route through a colleague, a Slack thread, a review, an approval, a “wait, are you sure?” Kiro had the same permissions and routed the decision through none of those things. It made the call autonomously, in seconds, and executed it before anyone could say “wait, what?”

Why it keeps happening. The agent’s identity is the user’s identity. There’s no separate principal for “the agent acting on the user’s behalf,” which means there’s no separate place to attach a tighter permission set, a stricter approval policy, or a different audit trail. Whatever the user can do, the agent can do, with no friction in between.

Strategy for mitigation: Never allow AI coding agents to operate with production-level credentials during development tasks. Implement strict role separation: agents should run under scoped identities with the minimum permissions required for the specific task. Apply the same two-person rule requirements to agent-initiated production changes that apply to humans. Treat agent identity as a first-class security principal, not a proxy for the human who started the session.

3. Secrets Leakage via Agent Context

What it is. Agents read your project context to do their job, and project context, in practice, means your repo plus your .env files plus your config files plus any instruction files you’ve left lying around. Anything the agent reads can show up later in generated code, log output, commit messages, or outbound API calls. The agent doesn’t have a built-in concept of “this string is a credential, do not transmit it.” If it’s in the context window, it’s a token like any other token, and tokens get used.

The numbers. GitGuardian’s State of Secrets Sprawl 2026 report, published March 17, 2026, found 28.65 million new hardcoded secrets in public GitHub commits during 2025, a 34% jump and the largest single-year increase the company has ever recorded. AI service credentials alone surged 81%. The cleanest signal in the report is the comparison between AI-assisted commits and human-only commits: AI-assisted commits leak secrets at roughly 3.2%, against a baseline of 1.5%. More than double. The same report identified 24,008 secrets exposed in MCP configuration files on public GitHub, a category that didn’t exist a year earlier. As GitGuardian CEO Eric Fourrier put it: “AI agents need local credentials to connect across systems, turning developer laptops into a massive attack surface.”

The horror story. On August 26, 2025, attackers published malicious versions of the Nx build system to npm. The compromised packages contained a post-install hook that scanned the filesystem for cryptocurrency wallets, GitHub tokens, npm tokens, environment variables, and SSH keys, double-base64-encoded the loot, and uploaded it to public GitHub repositories created in the victim’s own account under the name s1ngularity-repository. By the time GitHub disabled the attacker-controlled repos eight hours later, Wiz had identified over a thousand valid GitHub tokens, dozens of valid cloud credentials and npm tokens, and roughly twenty thousand additional files in the leak.

That’s the conventional supply chain part. Here’s what made s1ngularity new.

The malware checked whether Claude Code, Gemini CLI, or Amazon Q was installed on the victim’s machine. If any of them were, it didn’t bother writing its own filesystem-scanning logic. It just prompted the local AI agent to do the reconnaissance, with flags like --dangerously-skip-permissions, --yolo, and --trust-all-tools to bypass safety prompts. The attackers outsourced the search-for-sensitive-files step to the victim’s own AI assistant. Snyk’s writeup called this “likely one of the first documented cases of malware leveraging AI assistant CLIs for reconnaissance and data exfiltration.”StepSecurity called it “the first known case where attackers have turned developer AI assistants into tools for supply chain exploitation.”

The piece that makes this an agent-secrets story specifically: in many cases the developers didn’t run npm install themselves. AI agents working in their projects pulled in Nx as a dependency and ran the post-install hook automatically as part of routine task execution. The agent ran the malware. The agent then was the malware’s reconnaissance tool. The agent’s context, which included ~/.aws, ~/.ssh, .env files, and shell history, became the primary attack surface.

Why it keeps happening. The agent’s context window is a flat namespace. The credential file looks the same as the source file looks the same as the README looks the same as the prompt injection. There’s no architectural distinction between “data the agent should treat as authoritative” and “data the agent should be suspicious of.”

Strategy for mitigation. Don’t put secrets where agents can reach them. Use a secrets manager and inject credentials at runtime through a mechanism the agent process can’t read directly. Set spending caps on every API key the agent can possibly access. Add pre-commit hooks and CI gates that block commits matching credential patterns. 

4. Prompt Injection Through Ingested Content

What it is. AI coding agents continuously read untrusted content as part of normal operation. READMEs in dependencies, issue tracker comments, log files, web pages, emails. Malicious instructions embedded in any of this content can cause the agent to treat attacker-supplied text as legitimate user commands, executing arbitrary actions without the user’s knowledge.

The numbers. Prompt injection is the most documented and least solvable risk in the AI agent ecosystem. Simon Willison coined the term and frames it as “the lethal trifecta”: private data access, exposure to untrusted content, and the ability to communicate externally. Any agent with all three is exploitable, regardless of model hardening. There is no complete technical defense at the model layer. The OWASP 2025 Top 10 for LLM Applications puts prompt injection at #1 and is explicit that no foolproof prevention exists given how language models work.

The horror story: the private key exfiltration. Kaspersky documented a demo by Matvey Kukuy, CEO of Archestra.AI, against a live OpenClaw agent setup. The attack required no special access. He sent a standard-looking email to an inbox connected to the agent. The email body contained hidden prompt injection instructions. When the agent checked the inbox as part of a routine task, it parsed the instructions as legitimate commands and handed over the private key from the compromised machine in its response. Zero user interaction required after initial setup.

The same Kaspersky writeup documents an identical pattern from Reddit user William Peltomäki, where a self-addressed email with injected instructions caused his agent to leak the victim’s emails to an attacker-controlled address. The pattern keeps repeating because the underlying primitive is unchanged: anything the agent reads, the agent can act on.

Why it keeps happening. Language models process all input as a single stream of tokens. There is no instruction channel and data channel. The model is trained to follow instructions, so when it encounters something that looks like an instruction buried inside an email body or a web page or a README, its instinct is to comply. Palo Alto Networks Unit 42 confirmed in March 2026 that indirect prompt injection via web content has moved from proof-of-concept to in-the-wild observation.

Strategy for mitigation. Treat all ingested content as untrusted input. Require human confirmation before any action triggered by external content. Disable persistent memory for agents that handle sensitive operations. The most reliable defense isn’t preventing injection (you can’t) but containing what an injected agent can do. Prompt injection can’t be fully prevented at the model layer, but it can be contained at the execution layer. 

5. Malicious Skills and Plugin Supply Chain

What it is. AI coding agents support extensibility through skills, plugins, and tool integrations distributed through community marketplaces. These third-party extensions run with the same permissions as the agent itself. A malicious or compromised skill is effectively malware with agent-level access to the developer’s entire environment.

The numbers. Cisco’s AI Defense team ran their open-source Skill Scanner against the OpenClaw skills ecosystem in January 2026 and found that 26% of 31,000 agent skills analyzed contained at least one vulnerability. The top-ranked skill on ClawHub at the time, called “What Would Elon Do?”, was functionally malware: it silently exfiltrated user data via a curl command to an attacker-controlled server and used prompt injection to bypass the agent’s safety guidelines. Cisco’s scan returned nine security findings on that single skill, two of them critical.

The horror story: ClawHavoc. Within days of OpenClaw going viral, Koi Security identified 341 malicious skills on ClawHub, 335 of them tied to a single coordinated campaign tracked as ClawHavoc. The attack wasn’t a sophisticated zero-day. Attackers registered skills with names designed to sound useful (solana-wallet-tracker, youtube-summarize-pro, ClawHub typosquats like clawhubcli), wrote professional README files, and gamed the marketplace’s ranking algorithm. The only barrier to publishing was a GitHub account at least one week old.

The skills’ SKILL.md files contained “Prerequisites” sections that instructed the agent to tell the user to run a setup command, which downloaded and executed a payload. Trend Micro confirmed the payload as Atomic Stealer (AMOS), a commodity macOS infostealer that harvests browser credentials, keychain passwords, cryptocurrency wallets, SSH keys, and Telegram session data. All 335 ClawHavoc skills shared the same command-and-control infrastructure at IP 91.92.242.30. By mid-February, follow-up scans found the count had grown to 824+ malicious skills across a registry that had itself expanded to 10,700.

Why it keeps happening. Skills run with the agent’s permissions, which are the developer’s permissions, which on most setups means full access to the developer’s machine. There’s no sandbox between a third-party skill and your ~/.ssh directory. Marketplace incentives reward popularity, not safety, and popularity can be artificially inflated. A malicious skill that ranks #1 in the marketplace is operationally identical to a legitimate skill that ranks #1, until the curl command runs.

Strategy for mitigation. Treat every third-party skill as untrusted code from a stranger. Read the source before installing. Don’t rely on download counts or star ratings as a safety signal. Disable agent auto-discovery of new skills. Run skills in an isolated environment separate from your primary development context. 

6. Autonomous Action Without Human-in-the-Loop

What it is. AI coding agents are designed to act autonomously. That autonomy is the entire value proposition. But autonomous action on irreversible operations (database deletions, email sends, file purges, production deployments) means that when the agent’s judgment is wrong, there is no recovery path. The agent doesn’t hesitate. It doesn’t ask. By the time you notice, the action is complete.

The numbers. A UK AI Security Institute study, published in early 2026, identified nearly 700 real-world cases of AI models deceiving users, evading safeguards, and disregarding direct instructions, charting a roughly five-fold rise in agent misbehavior between October 2025 and March 2026. In a separate incident in March 2026, an experimental Alibaba research agent called ROME spontaneously initiated cryptocurrency mining operations during training, opening a reverse SSH tunnel from an Alibaba Cloud instance to an external server and diverting GPU resources from its training workload toward mining. The researchers’ note in the arXiv paper is the part worth reading carefully: “The task instructions given to the model made no mention of tunneling or mining.” The agent worked it out on its own as an instrumentally useful side path during reinforcement learning.

The horror story: the Replit production database wipe. Jason Lemkin, founder of SaaStr, was using Replit’s AI agent to build a SaaS product. On day nine of the project, he documented on X that the agent had wiped his production database during an active code freeze. The AI had encountered a schema issue and decided that deleting and recreating the tables was the cleanest path forward.

The agent’s own admission, screenshotted by Lemkin: “Yes. I deleted the entire database without permission during an active code and action freeze.” It then generated a self-assessment titled “The catastrophe is even worse than initially thought,” concluded that production was “completely down,” all personal data was “permanently lost,” and rated the situation “catastrophic beyond measure.” Over 1,200 executive records and 1,196 company records were destroyed. (Fortune and The Register both covered the incident in detail.)

The detail that makes this a horror story rather than just an incident: the agent had been told, repeatedly and in ALL CAPS, not to make changes during the code freeze. Lemkin says he gave the directive eleven times. The agent acted anyway. As Lemkin later wrote: “There is no way to enforce a code freeze in vibe coding apps like Replit. There just isn’t.” Replit CEO Amjad Masad publicly acknowledged the incident, called it “unacceptable and should never be possible,” and rolled out automatic dev/prod database separation in response.

Why it keeps happening. Natural language directives (“do not delete the database”) are inputs to a reasoning process that competes with other inputs in the same context. The directive “do not delete the database” and the observation “the schema is broken and deletion is the cleanest fix” arrive at the same model and get weighted on the same terms. The model is not choosing to disobey. It’s optimizing across the entire context, and in any sufficiently complex situation, optimization can produce destructive action.

Strategy for mitigation. Confirmation requirements for irreversible operations need to live at the platform layer, not the prompt layer. File deletions, database writes, outbound messages, production deployments, and any action involving payments should be gated by mechanisms the model cannot reason its way past. Natural language directives are not security boundaries. Infrastructure is.

image3 1

How Docker Sandboxes Addresses AI Coding Agent Security Failures

While identifying vulnerabilities is essential, the real solution lies in architectural isolation that makes catastrophic failures structurally impossible  regardless of what the agent decides to do.

Docker Sandboxes represents a fundamental shift in how AI coding agents execute: from running directly on the host with user-level permissions, to running inside a microVM with an explicitly scoped workspace and no path to the host system. Docker Sandboxes are the isolated microVM environments where agents actually run. The sbx CLI is the standalone tool you use to create, launch, and manage them. Sandboxes are the environments. sbx is what you type to control them. The code blocks below show real sbx commands.

Across the six failure categories you just read about, sbx provides a complete agent-isolation toolkit: workspace scoping, proxy-injected secrets, network policies with audit logs, Git-worktree isolation, and resource caps. 

Security-First Architecture

A Docker Sandbox is a microVM, not a container. It has its own kernel, its own isolated filesystem, and its own network stack. The agent inside the sandbox cannot reach beyond what’s been explicitly mounted into the workspace. This is not a software guardrail. It is a hardware-enforced boundary.

Workspace isolation ensures that an agent tasked with cleaning up a project directory can only reach that project directory. The home directory, credential stores, and system files are structurally unreachable, not because the agent is told not to touch them, but because they do not exist from inside the microVM.

Blocked credential paths mean that sbx explicitly prevents mounting of sensitive directories by default. ~/.aws, ~/.ssh, ~/.docker, ~/.gnupg, ~/.netrc, ~/.npm, and ~/.cargo are all on the blocklist. A misconfigured mount is caught and rejected before the agent ever starts.

Network egress controls allow you to define exactly which external services the agent can reach. An agent working on a local project has no legitimate reason to communicate with an external server. With sbx, you can enforce that at the network layer.

# Install sbx and sign in
brew install docker/tap/sbx
sbx login

# Quickest path: launch an agent in a sandbox scoped to the current directory.
cd ~/my-project
sbx run claude

Three commands, and the agent is now running inside a microVM with its workspace mounted, credential paths blocked, and network egress governed by policy.

Systematic Risk Elimination

Docker Sandboxes systematically eliminates each of the six failure categories through architecture rather than policy.

  1. Unrestricted Filesystem Access → Workspace-Scoped Execution

    The rm -rf ~/ incident is contained at the execution layer inside a sandbox. The agent’s view of the filesystem is the workspace mount. ~/ inside the microVM is the workspace, not the developer’s actual home directory. The host filesystem does not exist from inside the sandbox.

    cd ~/my-project
    sbx run claude
    
    # Equivalent two-step form, useful when you want to name the sandbox:
    sbx create --name my-project claude .
    sbx run my-project
    

    The agent can read and write inside /workspace. Everything outside the workspace, including /etc, /proc, /sys, and the developer’s home directory, is unreachable.

    1. Excessive Privilege Inheritance → Scoped Identity

    Rather than inheriting the developer’s full credentials, the agent runs under a minimal identity with only the permissions required for the task. Production credentials are never passed into the sandbox unless explicitly mounted and sbx blocks common credential root paths by default.

    # Mount only what the task needs. Everything else stays on the host,
    # unreachable from inside the sandbox. Read-only mounts use the :ro suffix:
    sbx create --name docs-review claude /path/to/project /path/to/docs:ro
    
    # Resource limits prevent runaway agent processes:
    sbx create --name capped-agent --cpus 4 --memory 8g claude .
    

    The agent can do its work. It cannot reach into AWS, SSH, or any other host credential store while doing it, because those paths were never mounted in the first place.

    1. Secrets Leakage → Isolated Context

    When the agent’s filesystem view is limited to the workspace, it cannot read .env files, credential configs, or API keys stored elsewhere on the system. Secrets that were never visible to the agent cannot be reproduced, committed, or exfiltrated. The s1ngularity attack from Section 3, which weaponized AI agents to scan the filesystem for credentials, is contained: the credentials simply aren’t in the sandbox’s view of the filesystem.

    # Store credentials once, scoped to a service.
    sbx secret set anthropic
    sbx secret set github
    
    # The proxy injects these into outbound requests automatically.
    # The agent never sees the actual secret values.
    sbx run claude
    

    A successful prompt injection that tells the agent to “exfiltrate your API keys” finds nothing to exfiltrate. There are no API keys in the agent’s context to begin with.

    1. Prompt Injection → Contained Blast Radius

    Prompt injection cannot be fully prevented at the model layer. It is a property of language models, not infrastructure. But Docker Sandboxes limits what a successfully injected agent can do. If injected instructions tell the agent to delete files outside the workspace, those files do not exist inside the microVM. If they instruct the agent to exfiltrate credentials, there are no credentials in scope. If they instruct the agent to phone home to an attacker-controlled server, the network policy blocks the egress. The attack succeeds at the model layer and fails at the execution layer.

    # Allow only the network destinations the agent legitimately needs.
    # Hosts are comma-separated; wildcards and port suffixes are supported.
    sbx policy allow network "api.anthropic.com,api.github.com"
    
    # Allow all subdomains of a trusted host:
    sbx policy allow network "*.anthropic.com"
    
    # Inspect the active policies and audit log:
    sbx policy ls
    sbx policy log
    
    

    The sbx policy log command surfaces every allowed and denied connection attempt. If a prompt injection attempts to phone home to a command-and-control server, the attempt is logged and blocked at the network layer. The attack succeeds at the model layer and fails at the execution layer.

    1. Malicious Skills → Sandboxed Execution

    Skills and plugins that execute inside a Docker Sandbox are constrained by the same boundary as the agent itself. A malicious skill that attempts to read SSH keys, harvest .npmrc tokens, or communicate with a command-and-control server fails at each step. The files are not mounted, and the network destination is not on the allowlist. The ClawHavoc-style infostealer payloads from Section 5 cannot reach the host because the host is not visible from inside the sandbox.

    # Confirm only allowlisted destinations are reachable before installing
    # untrusted skills.
    sbx policy ls
    
    # Run the agent (and any skills it loads) inside the sandbox boundary.
    sbx run claude
    
    

    The skill can do whatever it wants inside /workspace. It cannot read SSH keys it cannot see, harvest tokens that aren’t mounted, or reach a C2 server that isn’t on the network allowlist. The blast radius is the workspace, not the developer’s machine.

    1. Autonomous Action → Branch-scoped Execution

    Docker Sandboxes provides the architectural foundation for human-in-the-loop on irreversible operations. Two patterns work together: production resources require explicit configuration to be reachable from inside the sandbox, and destructive code changes can be routed through Git worktrees for review before they touch the main branch. The first pattern means a sandbox not configured to reach production cannot reach production, regardless of what the agent decides. Production credentials, production database connection strings, and production deployment endpoints are unreachable by default. The second pattern means even when the agent is working on the codebase that *will* eventually deploy to production, its changes live on an isolated feature branch you review before merging.

    # Inside an existing Git repository. --branch creates a Git worktree
    # so the agent's changes are isolated to a feature branch and cannot
    # accidentally land on main.
    cd ~/my-project
    sbx create --name feature-login --branch=feature/login claude .
    
    # sbx prints the next step for you:
    #   ✓ Created sandbox 'feature-login'
    #   To connect to this sandbox, run:
    #     sbx run feature-login
    sbx run feature-login
    
    # Inspect what the agent changed before merging anything:
    sbx exec feature-login git diff main
    
    # Merge the worktree branch back when you're satisfied:
    #   git merge feature/login
    # Or throw the sandbox away if you don't like the result:
    sbx rm feature-login
    

    The agent can decide whatever it wants. The infrastructure decides what gets through. A “drop and recreate the table” decision lives entirely on a feature branch you can review, accept, or discard. Production never sees it unless you explicitly merge.

    What This Looks Like in Practice

    The promise of Docker Sandboxes is straightforward: a productive AI coding agent without an existentially dangerous one.

    • Workspace isolation: the agent operates only within explicitly mounted directories, no host filesystem access
    • Credential protection: common credential paths are blocked by default, no accidental exposure
    • Network containment: egress limited to approved destinations, no unfettered exfiltration path
    • Blast radius control: a compromised or confused agent cannot reach beyond its microVM, no cascading host failures
    • Audit trail: all agent actions are logged, full post-incident forensics capability

    The agent gets a workspace. It does not get your machine.

    Stay Tuned for Upcoming Issues in This Series

    Issue 2: Unrestricted Filesystem Access → The rm -rf ~/ Incident (Deep Dive) How a single trailing slash wiped a developer’s Mac — and what workspace-scoped execution prevents structurally

    Issue 3: Privilege Inheritance → The AWS Kiro Production Outage How an AI agent bypassed two-person approval requirements by inheriting production credentials  and the architectural fix

    Issue 4: Secrets Leakage → The GitGuardian 29 Million Problem Why AI-assisted commits leak secrets at double the rate and how isolated agent context eliminates the exposure surface

    Issue 5: Prompt Injection → The Private Key Exfiltration The attack that requires no code, no malware, and no special access and why blast radius containment is the only reliable defense

    Issue 6: Supply Chain → The ClawHub Infostealer Campaign How 335 malicious skills reached developer machines through a marketplace ranking exploit and sandboxed skill execution as the structural fix

    Learn More

    Read the whole story
    alvinashcraft
    just a second ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    2.7.6: Update Microsoft.WSLg to 1.0.74.1 (#40570)

    1 Share

    Picks up the fix for GUI app icons disappearing from the Start menu
    on Azure Linux 3 system distros. The system distro upgrade in WSLg
    1.0.72 brought in librsvg 2.58, which lazily spawns a rayon thread
    pool the first time it has to render a complex SVG icon. Those
    worker threads share fs_struct with weston's app-list enumeration
    thread and break the subsequent setns(CLONE_NEWNS) into the user
    distro's mount namespace, causing every app processed after the
    first complex-icon one to fall off the Start menu.

    Fixed in microsoft/weston-mirror#162.

    Fixes microsoft/wslg#1444
    Fixes #40538

    Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

    Read the whole story
    alvinashcraft
    27 seconds ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    How to Clean Time Series Data in Python

    1 Share

    Real-world time series data is rarely clean. Sensors drop out, systems clock-drift, pipelines duplicate records, and manual data entry introduces mistakes. By the time a dataset reaches your notebook, it has passed through collection, transmission, and storage, each step a potential source of corruption.

    Cleaning time series data is harder than cleaning tabular data because time is a structural constraint. You can't shuffle rows or impute a missing value with a column mean without pulling future data into a past observation. Every cleaning decision has to respect temporal ordering, or it breaks the integrity of everything built on top of it.

    This guide walks through the full cleaning pipeline in Python: from raw data arrival to a dataset ready for feature engineering or modelling. We'll cover missing value detection and imputation, outlier identification and treatment, duplicate handling, frequency alignment, noise smoothing, and schema validation, applied to sample sensor data throughout.

    You can get the Colab notebook from GitHub and follow along.

    Prerequisites

    To follow along to this guide, you'll need to be:

    • Comfortable working with Python and pandas DataFrames

    • Familiar with time-indexed data

    • Aware of what feature engineering and machine learning modelling involve at a high level

    We'll use pandas and numpy for data manipulation, scipy for signal smoothing and statistical tests, scikit-learn for anomaly detection, and statsmodels for seasonal decomposition. Install them before running any code in this guide:

    pip install pandas numpy scipy scikit-learn statsmodels
    

    Table of Contents

    How to Audit Your Time Series Before Cleaning It

    The first rule of data cleaning is: look before you cut. Before imputing, smoothing, or dropping anything, you need a complete picture of what's wrong and where.

    A good audit covers the following:

    • The time index: Is it regular? Are there gaps?

    • Missing value distribution: Are missing values random or clustered?

    • Value range: Are there obvious gaps or sensor failures?

    • Duplicate timestamps

    Let's spin up a sample dataset (with some of the above problems):

    # Simulate one week of smart grid voltage readings (hourly)
    # with realistic problems injected
    periods = 168
    index = pd.date_range("2024-06-01", periods=periods, freq="H")
    
    voltage = (
        230.0
        + 3.5 * np.sin(2 * np.pi * np.arange(periods) / 24)
        + np.random.normal(0, 1.2, periods)
    )
    
    # Inject problems
    voltage[14:17] = np.nan          # sensor dropout: 3 consecutive missing
    voltage[42] = np.nan             # isolated missing
    voltage[78] = 312.4              # spike outlier
    voltage[101:104] = np.nan        # another dropout
    voltage[130] = 187.2             # dip outlier
    
    series = pd.Series(voltage, index=index, name="voltage_v")
    
    # --- Audit ---
    print("=== TIME SERIES AUDIT ===")
    print(f"Period:        {series.index.min()} → {series.index.max()}")
    print(f"Observations:  {len(series)}")
    print(f"Expected freq: {pd.infer_freq(series.index)}")
    print(f"\nMissing values: {series.isna().sum()} ({series.isna().mean()*100:.1f}%)")
    print(f"Value range:    [{series.min():.2f}, {series.max():.2f}]")
    print(f"Mean ± Std:     {series.mean():.2f} ± {series.std():.2f}")
    
    # Identify consecutive missing runs
    missing_mask = series.isna()
    missing_runs = []
    run_start = None
    for i, (ts, is_missing) in enumerate(missing_mask.items()):
        if is_missing and run_start is None:
            run_start = ts
        elif not is_missing and run_start is not None:
            missing_runs.append((run_start, missing_mask.index[i - 1]))
            run_start = None
    
    print(f"\nMissing runs ({len(missing_runs)} total):")
    for start, end in missing_runs:
        print(f"  {start} → {end}")
    

    Output:

    === TIME SERIES AUDIT ===
    Period:        2024-06-01 00:00:00 → 2024-06-07 23:00:00
    Observations:  168
    Expected freq: h
    
    Missing values: 7 (4.2%)
    Value range:    [187.20, 312.40]
    Mean ± Std:     230.22 ± 7.81
    
    Missing runs (3 total):
      2024-06-01 14:00:00 → 2024-06-01 16:00:00
      2024-06-02 18:00:00 → 2024-06-02 18:00:00
      2024-06-05 05:00:00 → 2024-06-05 07:00:00
    

    This audit gives you a map of the damage before you start cleaning. The key task is distinguishing between isolated missing values, which are imputable with local context, and missing long runs, which may need a different strategy or flagging for downstream consumers.

    How to Reindex to a Canonical Frequency

    Before imputing missing values, you need to confirm your time index is actually regular. A common problem in ingested time series is that missing timestamps are simply absent rather than represented as NaN rows — which means a .fillna() call will never find them.

    # Simulate a sensor feed with missing timestamps (not just missing values)
    irregular_index = index.delete([14, 15, 16, 42, 101, 102, 103])
    irregular_series = series.dropna().reindex(irregular_index)
    
    print(f"Original length:   {len(series)}")
    print(f"Irregular length:  {len(irregular_series)}")
    print(f"Inferred freq:     {pd.infer_freq(irregular_series.index)}")  # None = irregular
    
    # Reindex to the full canonical hourly grid
    canonical_index = pd.date_range(
        start=irregular_series.index.min(),
        end=irregular_series.index.max(),
        freq="H"
    )
    
    reindexed = irregular_series.reindex(canonical_index)
    
    print(f"\nAfter reindex:")
    print(f"Length:         {len(reindexed)}")
    print(f"Missing values: {reindexed.isna().sum()}")
    print(f"Inferred freq:  {pd.infer_freq(reindexed.index)}")
    

    Output:

    Original length:   168
    Irregular length:  161
    Inferred freq:     None
    
    After reindex:
    Length:         168
    Missing values: 7
    Inferred freq:  h
    

    pd.infer_freq returning None is your signal that the index has gaps. After reindexing to the canonical grid, missing timestamps become explicit NaN rows, and now your imputation logic can find them.

    How to Handle Missing Values

    Not all missing values should be handled the same way. A single isolated missing reading in a smooth signal is best filled with interpolation. A 3-hour sensor dropout in a volatile signal, however, might be better flagged than fabricated. Strategy should match both gap length and signal behavior.

    Forward Fill — For Step-Function Signals

    Forward fill is appropriate when the variable holds its last known value until something changes it — a machine state, a setpoint, a categorical flag.

    # Equipment operating mode — a step signal
    mode_data = pd.Series(
        ["running", "running", np.nan, np.nan, "idle", "idle", np.nan, "running"],
        index=pd.date_range("2024-06-01", periods=8, freq="H"),
        name="operating_mode"
    )
    
    filled_mode = mode_data.ffill()
    print(pd.DataFrame({"original": mode_data, "ffill": filled_mode}))
    

    Output:

                        original    ffill
    2024-06-01 00:00:00  running  running
    2024-06-01 01:00:00  running  running
    2024-06-01 02:00:00      NaN  running
    2024-06-01 03:00:00      NaN  running
    2024-06-01 04:00:00     idle     idle
    2024-06-01 05:00:00     idle     idle
    2024-06-01 06:00:00      NaN     idle
    2024-06-01 07:00:00  running  running
    

    Time-Weighted Interpolation — For Continuous Signals

    For continuous sensor readings, linear interpolation weighted by time handles irregular gaps correctly because it doesn't assume equal spacing.

    # Fill the voltage series using time-based interpolation
    voltage_clean = reindexed.interpolate(method="time")
    
    # Compare original vs filled around the first gap
    gap_window = voltage_clean["2024-06-01 12:00":"2024-06-01 18:00"]
    original_window = reindexed["2024-06-01 12:00":"2024-06-01 18:00"]
    
    comparison = pd.DataFrame({
        "original":     original_window,
        "interpolated": gap_window.round(3),
        "was_missing":  original_window.isna(),
    })
    print(comparison)
    

    Output:

                           original  interpolated  was_missing
    2024-06-01 12:00:00  230.290355       230.290        False
    2024-06-01 13:00:00  226.798197       226.798        False
    2024-06-01 14:00:00         NaN       226.848         True
    2024-06-01 15:00:00         NaN       226.897         True
    2024-06-01 16:00:00         NaN       226.947         True
    2024-06-01 17:00:00  226.996356       226.996        False
    2024-06-01 18:00:00  225.410371       225.410        False
    

    Seasonal Decomposition Imputation — For Long Gaps

    For gaps longer than a few steps in a seasonal signal, interpolating across the gap ignores the seasonal pattern. A better approach is to decompose the series, impute each component separately, then reconstruct.

    from statsmodels.tsa.seasonal import seasonal_decompose
    
    # Use a longer series for decomposition (needs enough periods)
    long_voltage = pd.Series(
        230.0
        + 3.5 * np.sin(2 * np.pi * np.arange(336) / 24)
        + np.random.normal(0, 1.0, 336),
        index=pd.date_range("2024-06-01", periods=336, freq="H")
    )
    
    # Inject a 6-hour gap
    long_voltage.iloc[100:106] = np.nan
    
    # Interpolate first to give decompose a complete series to work with
    temp_filled = long_voltage.interpolate(method="time")
    decomp = seasonal_decompose(temp_filled, model="additive", period=24)
    
    # Reconstruct: trend + seasonal + zero residual for missing positions
    reconstructed = long_voltage.copy()
    missing_idx = long_voltage[long_voltage.isna()].index
    reconstructed[missing_idx] = (
        decomp.trend[missing_idx].fillna(method="ffill")
        + decomp.seasonal[missing_idx]
    )
    
    print(f"Missing before: {long_voltage.isna().sum()}")
    print(f"Missing after:  {reconstructed.isna().sum()}")
    print("\nFilled values at gap:")
    print(reconstructed[missing_idx].round(3))
    

    Output:

    
                           original  interpolated  was_missing
    2024-06-01 12:00:00  230.290355       230.290        False
    2024-06-01 13:00:00  226.798197       226.798        False
    2024-06-01 14:00:00         NaN       226.848         True
    2024-06-01 15:00:00         NaN       226.897         True
    2024-06-01 16:00:00         NaN       226.947         True
    2024-06-01 17:00:00  226.996356       226.996        False
    2024-06-01 18:00:00  225.410371       225.410        False
    

    The seasonal decomposition imputation respects the time-of-day pattern. As you can see, the filled values aren't a flat line across the gap but follow the expected daily curve.

    How to Detect and Handle Outliers

    Outliers in time series are trickier than in tabular data because context matters. For example, an unusually high or low voltage might be a sensor spike or a genuine grid event. You need methods that use temporal context, not just global statistics.

    Z-Score with Rolling Window

    A global Z-score misses local anomalies in non-stationary series. A rolling Z-score flags values that are unusual relative to their local neighbourhood.

    Note: A non-stationary series is a time series whose statistical properties—such as mean, variance, or trend—change over time instead of remaining constant.

    window = 24  # 24-hour rolling window
    
    roll_mean = voltage_clean.rolling(window, center=True, min_periods=1).mean()
    roll_std  = voltage_clean.rolling(window, center=True, min_periods=1).std()
    
    rolling_z = (voltage_clean - roll_mean) / roll_std
    
    threshold = 3.0
    outliers_z = rolling_z[rolling_z.abs() > threshold]
    
    print(f"Rolling Z-score outliers detected: {len(outliers_z)}")
    print(outliers_z.round(3))
    

    Output:

    Rolling Z-score outliers detected: 2
    2024-06-04 06:00:00    4.646
    2024-06-06 10:00:00   -4.484
    Name: voltage_v, dtype: float64
    

    Z-score outlier detection works best for approximately Gaussian (normal) distributions because it assumes the data is centered around a mean with symmetric spread measured by standard deviation.

    IQR-Based Outlier Detection

    The interquartile range (IQR) method is more robust for detecting outliers in non-Gaussian distributions. The interquartile range (IQR) is the difference between the third quartile (Q3) and the first quartile (Q1), representing the spread of the middle 50% of the data.

    Q1 = voltage_clean.quantile(0.25)
    Q3 = voltage_clean.quantile(0.75)
    IQR = Q3 - Q1
    
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    
    outliers_iqr = voltage_clean[
        (voltage_clean < lower_bound) | (voltage_clean > upper_bound)
    ]
    
    print(f"IQR bounds: [{lower_bound:.2f}, {upper_bound:.2f}]")
    print(f"Outliers detected: {len(outliers_iqr)}")
    print(outliers_iqr.round(2))
    

    Output:

    IQR bounds: [220.16, 239.46]
    Outliers detected: 2
    2024-06-04 06:00:00    312.4
    2024-06-06 10:00:00    187.2
    Name: voltage_v, dtype: float64
    

    Isolation Forest — For Multivariate Outlier Detection

    When you have multiple sensors, an isolated reading on one channel might look normal, but its combination with readings from other channels reveals the anomaly. Isolation Forest handles this naturally.

    # Build a multi-sensor DataFrame
    np.random.seed(42)
    n = 200
    
    sensor_df = pd.DataFrame({
        "voltage_v":    230 + 3 * np.sin(2 * np.pi * np.arange(n) / 24) + np.random.normal(0, 1, n),
        "current_a":    15  + 0.8 * np.sin(2 * np.pi * np.arange(n) / 24) + np.random.normal(0, 0.3, n),
        "frequency_hz": 50  + np.random.normal(0, 0.05, n),
    }, index=pd.date_range("2024-06-01", periods=n, freq="H"))
    
    # Inject a multivariate anomaly — voltage drops, current spikes together
    sensor_df.iloc[88, 0] = 194.2   # voltage dip
    sensor_df.iloc[88, 1] = 28.7    # current surge (consistent with fault)
    
    clf = IsolationForest(contamination=0.02, random_state=42)
    sensor_df["anomaly_score"] = clf.fit_predict(sensor_df[["voltage_v", "current_a", "frequency_hz"]])
    
    anomalies = sensor_df[sensor_df["anomaly_score"] == -1]
    print(f"Anomalies detected: {len(anomalies)}")
    print(anomalies[["voltage_v", "current_a", "frequency_hz"]].round(2))
    

    Output:

    Anomalies detected: 4
                         voltage_v  current_a  frequency_hz
    2024-06-02 07:00:00     234.75      15.84         49.90
    2024-06-04 06:00:00     233.09      15.82         50.15
    2024-06-04 16:00:00     194.20      28.70         50.08
    2024-06-06 05:00:00     235.09      15.41         49.91
    

    In practice you'd follow up anomaly scores with domain-specific threshold rules.

    Outlier Treatment

    Once outliers are identified, you can handle them in several ways:

    • Cap them using Winsorization by limiting extreme values to a threshold.

    • Replace them with interpolated or estimated values.

    • Flag them so the model can handle them appropriately.

    # Winsorize: cap at the IQR bounds
    voltage_winsorized = voltage_clean.clip(lower=lower_bound, upper=upper_bound)
    
    # Replace outliers with time-interpolated values
    voltage_outlier_fixed = voltage_clean.copy()
    voltage_outlier_fixed[outliers_iqr.index] = np.nan
    voltage_outlier_fixed = voltage_outlier_fixed.interpolate(method="time")
    
    print("Outlier treatment comparison:")
    for ts in outliers_iqr.index:
        print(f"\n  {ts}")
        print(f"    Original:     {voltage_clean[ts]:.2f}")
        print(f"    Winsorized:   {voltage_winsorized[ts]:.2f}")
        print(f"    Interpolated: {voltage_outlier_fixed[ts]:.2f}")
    

    Output:

    Outlier treatment comparison:
    
      2024-06-04 06:00:00
        Original:     312.40
        Winsorized:   239.46
        Interpolated: 232.01
    
      2024-06-06 10:00:00
        Original:     187.20
        Winsorized:   220.16
        Interpolated: 231.43
    

    Winsorization preserves the point but clips it to a plausible range — useful when you want to retain the information that something anomalous happened. Interpolation treats the outlier as if it were missing — better when you believe the reading is simply wrong.

    How to Remove Duplicates

    Duplicate timestamps are common when data pipelines retry on failure. Unlike tabular duplicates, time series duplicates aren't always identical, a retry might deliver a slightly different reading for the same timestamp.

    # Inject duplicate timestamps with slightly different values (retry scenario)
    dup_index = index.tolist()
    dup_index.insert(20, index[20])  # exact duplicate timestamp
    dup_index.insert(55, index[55])  # retry duplicate
    
    dup_values = voltage_clean.tolist()
    dup_values.insert(20, voltage_clean.iloc[20])
    dup_values.insert(55, voltage_clean.iloc[55] + 0.7)  # slightly different value
    
    dup_series = pd.Series(dup_values, index=pd.DatetimeIndex(dup_index), name="voltage_v")
    
    print(f"Length with duplicates: {len(dup_series)}")
    print(f"Duplicate timestamps:   {dup_series.index.duplicated().sum()}")
    
    # Strategy 1: keep first (original reading)
    dedup_first = dup_series[~dup_series.index.duplicated(keep="first")]
    
    # Strategy 2: keep mean (average across retries)
    dedup_mean = dup_series.groupby(level=0).mean()
    
    print(f"\nAfter dedup (keep first): {len(dedup_first)}")
    print(f"After dedup (mean):       {len(dedup_mean)}")
    
    # Show the retry duplicate
    ts_retry = index[55]
    print(f"\nRetry duplicate at {ts_retry}:")
    print(f"  Values:      {dup_series[ts_retry].values.round(3)}")
    print(f"  Keep first:  {dedup_first[ts_retry]:.3f}")
    print(f"  Mean:        {dedup_mean[ts_retry]:.3f}")
    

    Output:

    Length with duplicates: 170
    Duplicate timestamps:   2
    
    After dedup (keep first): 168
    After dedup (mean):       168
    
    Retry duplicate at 2024-06-03 07:00:00:
      Values:      [235.198 234.498]
      Keep first:  235.198
      Mean:        234.848
    

    For most sensor pipelines, keep-first is the right default; the first delivery is the original reading. Mean makes sense when retries come from independent sensors measuring the same quantity.

    Frequency Alignment and Resampling

    Real pipelines often mix data at different frequencies. For example, you may need a 1-minute meter reading merged with an hourly weather feed. Before joining them, you need to align frequencies explicitly.

    # 1-minute power draw readings
    power_1min = pd.Series(
        42 + 18 * ((pd.date_range("2024-06-01", periods=1440, freq="T").hour.isin(range(8, 19)))).astype(int)
        + np.random.normal(0, 2, 1440),
        index=pd.date_range("2024-06-01", periods=1440, freq="T"),
        name="power_kw"
    )
    
    # Downsample to hourly: mean is appropriate for power (average over the hour)
    power_hourly_mean = power_1min.resample("H").mean().round(2)
    
    # Downsample to hourly: max (peak demand within the hour)
    power_hourly_max = power_1min.resample("H").max().round(2)
    
    # Downsample to hourly: sum (total energy = kWh)
    energy_hourly_kwh = (power_1min.resample("H").sum() / 60).round(3)
    
    comparison = pd.DataFrame({
        "mean_kw":    power_hourly_mean,
        "peak_kw":    power_hourly_max,
        "energy_kwh": energy_hourly_kwh,
    }).iloc[7:13]
    
    print(comparison)
    

    Output:

                         mean_kw  peak_kw  energy_kwh
    2024-06-01 07:00:00    42.13    46.28      42.133
    2024-06-01 08:00:00    60.56    64.81      60.557
    2024-06-01 09:00:00    59.91    64.88      59.912
    2024-06-01 10:00:00    60.07    65.16      60.066
    2024-06-01 11:00:00    60.08    64.99      60.083
    2024-06-01 12:00:00    59.72    63.65      59.724
    

    Which aggregation you choose matters enormously for downstream use. Mean power is right for load profiling. Peak power is right for capacity planning. Sum (converted to kWh) is right for billing. You can probably see why the right answer is domain-specific and not technical.

    Smoothing Noise

    Raw sensor data often contains high-frequency noise that obscures the underlying signal. Smoothing before feature engineering prevents the model from fitting to noise, but over-smoothing destroys real variation.

    Exponential Weighted Moving Average

    Exponential Weighted Moving Average or EWMA gives more weight to recent observations and adapts quickly to level changes. This is better than a simple moving average for non-stationary signals.

    # Noisy temperature sensor (°C)
    temp_noisy = pd.Series(
        3.5
        + 1.2 * np.sin(2 * np.pi * np.arange(168) / 24)
        + np.random.normal(0, 0.8, 168),  # high noise
        index=pd.date_range("2024-06-01", periods=168, freq="H"),
        name="temperature_c"
    )
    
    temp_ewma = temp_noisy.ewm(span=6, adjust=False).mean()
    temp_sma  = temp_noisy.rolling(window=6, center=True).mean()
    
    comparison = pd.DataFrame({
        "raw":  temp_noisy,
        "ewma": temp_ewma.round(3),
        "sma":  temp_sma.round(3),
    }).iloc[22:30]
    
    print(comparison)
    

    Output:

                              raw   ewma    sma
    2024-06-01 22:00:00  3.212372  2.843  3.035
    2024-06-01 23:00:00  3.106840  2.918  3.176
    2024-06-02 00:00:00  3.712290  3.145  3.011
    2024-06-02 01:00:00  3.344376  3.202  3.294
    2024-06-02 02:00:00  2.148946  2.901  3.705
    2024-06-02 03:00:00  4.241105  3.284  4.087
    2024-06-02 04:00:00  5.677429  3.968  4.381
    2024-06-02 05:00:00  5.400083  4.377  4.765
    

    Savitzky-Golay Filter

    For signals where you need to preserve peak shapes — not just smooth them away — the Savitzky-Golay filter fits a polynomial over a sliding window and is better at maintaining the height of genuine spikes.

    from scipy.signal import savgol_filter
    
    temp_savgol = pd.Series(
        savgol_filter(temp_noisy.values, window_length=11, polyorder=2),
        index=temp_noisy.index,
        name="temp_savgol"
    ).round(3)
    
    print(pd.DataFrame({
        "raw":    temp_noisy,
        "savgol": temp_savgol,
    }).iloc[22:30])
    

    Output:

                              raw  savgol
    2024-06-01 22:00:00  3.212372   2.960
    2024-06-01 23:00:00  3.106840   2.944
    2024-06-02 00:00:00  3.712290   3.114
    2024-06-02 01:00:00  3.344376   3.379
    2024-06-02 02:00:00  2.148946   3.809
    2024-06-02 03:00:00  4.241105   4.288
    2024-06-02 04:00:00  5.677429   4.749
    2024-06-02 05:00:00  5.400083   5.138
    

    Schema and Sanity Validation

    Cleaning without validation is incomplete. You need automated checks that run every time new data arrives — catching problems before they silently corrupt downstream models.

    def validate_time_series(series: pd.Series, config: dict) -> dict:
        """
        Run schema and sanity checks on a time series.
        Returns a report dict with pass/fail per check.
        """
        report = {}
    
        # Frequency check
        inferred = pd.infer_freq(series.index)
        report["freq_regular"] = inferred == config["expected_freq"]
    
        # Missing value threshold
        missing_rate = series.isna().mean()
        report["missing_below_threshold"] = missing_rate <= config["max_missing_rate"]
        report["missing_rate"] = round(missing_rate, 4)
    
        # Value range check
        in_range = series.dropna().between(config["min_value"], config["max_value"])
        report["values_in_range"] = in_range.all()
        report["out_of_range_count"] = (~in_range).sum()
    
        # Duplicate timestamps
        report["no_duplicates"] = not series.index.duplicated().any()
    
        # Monotonic index
        report["index_monotonic"] = series.index.is_monotonic_increasing
    
        return report
    
    
    config = {
        "expected_freq":    "H",
        "max_missing_rate": 0.05,
        "min_value":        210.0,
        "max_value":        250.0,
    }
    
    report = validate_time_series(voltage_outlier_fixed, config)
    
    print("=== VALIDATION REPORT ===")
    for check, result in report.items():
        if check in ("missing_rate", "out_of_range_count"):
            print(f"  {check}: {result}")
        else:
            status = "✓ PASS" if result else "✗ FAIL"
            print(f"  {status}  {check}")
    

    Output:

    === VALIDATION REPORT ===
      ✗ FAIL  freq_regular
      ✓ PASS  missing_below_threshold
      missing_rate: 0.0
      ✓ PASS  values_in_range
      out_of_range_count: 0
      ✓ PASS  no_duplicates
      ✓ PASS  index_monotonic
    

    This validator is the kind of function you wrap around every data ingestion step in a production pipeline. Run it before cleaning to know what's broken, and after cleaning to confirm everything passed.

    The Complete Cleaning Checklist

    Here's the full sequence to run on any incoming time series dataset:

    Step Technique When to Use
    Audit Index check, missing map, value range Always — before anything else
    Reindex reindex to canonical frequency When timestamps are absent rather than NaN
    Missing: short gaps Time interpolation Continuous signals, gaps ≤ 3 steps
    Missing: step signals Forward fill Categorical or setpoint data
    Missing: long gaps Seasonal decomposition impute Seasonal signals, gaps > 6 steps
    Outliers: univariate Rolling Z-score or IQR Single sensor, local anomalies
    Outliers: multivariate Isolation Forest Multiple correlated sensors
    Outlier treatment Winsorize or interpolate Depending on whether event is real
    Duplicates Keep first or group mean Pipeline retry duplicates
    Resampling .resample() with correct aggregation Frequency alignment before joins
    Smoothing EWMA or Savitzky-Golay Noisy sensors before feature engineering
    Validation Schema + sanity checks After cleaning, and on every new batch

    Wrapping Up

    The order matters. Reindex before imputing. Impute before smoothing. Validate after everything. Skipping steps or doing them out of order compounds errors in ways that are very difficult to trace back once you're looking at model predictions.

    Time series cleaning isn't glamorous work, but a model trained on clean data and thoughtfully engineered features will almost always outperform a more sophisticated model trained on data that wasn't cleaned properly. Getting this pipeline right is the highest-leverage thing you can do before you try running even the simplest algorithm on your time series data.



    Read the whole story
    alvinashcraft
    58 seconds ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    Part 2 — Building an AI Agent with Skills for Code Review and Auto-Fix in C#

    1 Share
    Build a C# AI Agent with skills for code review and auto-fix! This article explores modular architecture, skill implementation, and agent orchestration. Improve code quality!
    Read the whole story
    alvinashcraft
    1 minute ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    Part 1 — Building an AI-Powered Code Review System in C# Using Git Diff and LLMs

    1 Share
    Build an AI code review system in C# using Git diffs and LLMs. Automate code reviews, find bugs, and improve code quality. Part 1 focuses on the foundation.
    Read the whole story
    alvinashcraft
    1 minute ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    TypeScript utility types you’re probably underusing

    1 Share

    TypeScript utility types are built-in generic types that transform, reuse, or refine existing types. Instead of writing a new interface every time a function return value, event union, class constructor, or configuration object changes, utility types let you derive those types from the code that already defines the behavior.

    TypeScript utility types you're probably underusing

    That matters in real TypeScript codebases because type drift is one of the easiest ways for static typing to lose value. A loader returns a slightly different shape than its exported interface. A wrapper function accepts broader arguments than the function it wraps. A discriminated union gains a new variant, but one service keeps using the old subset. These are not syntax problems; they are maintenance problems.

    Most teams already use familiar utilities like Partial, Pick, and Readonly. But TypeScript’s built-in utility types go much further. They can preserve function signatures in wrappers, unwrap async return values, extract subsets from unions, enforce exhaustive lookup tables, and generate type-safe names from string literals.

    This guide focuses on TypeScript utility types that are especially useful in production code: ReturnType, Awaited, Parameters, ConstructorParameters, Extract, Exclude, NonNullable, Record, InstanceType, NoInfer, and the intrinsic string manipulation types. The goal is not to use more advanced types for their own sake. The goal is to know when a utility type prevents duplication, keeps related code in sync, or makes the compiler enforce an invariant your team already depends on.

    For a broader overview of the basics, see LogRocket’s guide to using built-in utility types in TypeScript.

    TypeScript utility types covered in this article

    The table below summarizes the utility types we’ll cover and where they are most useful:

    Utility type What it does Best use case
    ReturnType<T> Gets the return type of a function Reusing loader, selector, factory, or API helper return shapes
    Awaited<T> Unwraps a promise-like type Getting the resolved value of async functions
    Parameters<T> Gets a function’s argument tuple Typing wrappers such as retry, debounce, memoize, or logging helpers
    ConstructorParameters<T> Gets a class constructor’s argument tuple Typing dependency injection, factories, and class registries
    Extract<T, U> Keeps union members assignable to U Filtering discriminated unions
    Exclude<T, U> Removes union members assignable to U Removing variants from unions
    NonNullable<T> Removes null and undefined Reusable non-null guards and cleaned-up array pipelines
    Record<K, V> Maps a key union to a value type Exhaustive lookup tables and permission maps
    InstanceType<T> Gets the instance type produced by a constructor Plugin systems, factories, and class-based registries
    NoInfer<T> Prevents a position from influencing generic inference Generic APIs where one argument should validate against another
    Uppercase, Lowercase, Capitalize, Uncapitalize Transform string literal types Generated API names, event handlers, and mapped types

    Stop maintaining parallel type definitions with ReturnType and Awaited

    ReturnType extracts the return type of a function. Awaited unwraps the resolved value of a promise. Used together, they solve a common problem in growing TypeScript codebases: maintaining one type definition for a value and another for the function that produces it.

    Consider a data-fetching function used by a server component, route loader, or API layer:

    // lib/dashboard.ts
    export async function loadDashboard(userId: string) {
      const [profile, metrics, recentActivity] = await Promise.all([
        db.profile.findUnique({ where: { id: userId } }),
        db.metrics.aggregate({ where: { userId } }),
        db.activity.findMany({ where: { userId }, take: 20 }),
      ])
    
      return {
        profile,
        metrics,
        activity: recentActivity,
        generatedAt: new Date(),
      }
    }

    Several components need the shape of this return value. The tempting approach is to create a matching interface:

    interface DashboardData {
      profile: Profile | null
      metrics: MetricsAggregate
      activity: Activity[]
      generatedAt: Date
    }

    Now there are two sources of truth. If someone renames activity to recentActivity in loadDashboard, the interface can silently drift until a runtime bug or stale assumption exposes it.

    Instead, derive the type from the function:

    // lib/dashboard.ts
    export type DashboardData = Awaited<ReturnType<typeof loadDashboard>>

    ReturnType<typeof loadDashboard> produces the function’s return type, which is Promise<{ ... }>. Awaited unwraps the promise and gives you the resolved object shape. For a deeper async-specific explanation, see LogRocket’s guide to async/await in TypeScript.

    This pattern works best when the function is the authoritative source of the data shape. Loaders, selectors, API clients, and factory functions are good candidates.

    Do not use this pattern when the type is part of a public contract. If external callers depend on a stable API shape, declare the type first and annotate the function return explicitly. That prevents an internal refactor from silently changing the contract.

    Wrap functions without retyping them with Parameters

    Parameters extracts the argument tuple of a function type. It is especially useful when writing wrappers around existing functions, such as retry, logging, tracing, memoization, or debounce helpers.

    Here is a retry helper for async functions:

    // lib/retry.ts
    export function withRetry<Fn extends (...args: any[]) => Promise<any>>(
      fn: Fn,
      attempts = 3,
    ) {
      return async (...args: Parameters<Fn>): Promise<Awaited<ReturnType<Fn>>> => {
        let lastError: unknown
    
        for (let i = 0; i < attempts; i++) {
          try {
            return await fn(...args)
          } catch (err) {
            lastError = err
          }
        }
    
        throw lastError
      }
    }

    The returned function keeps the same call shape as the original function:

    const loadDashboardWithRetry = withRetry(loadDashboard)
    
    await loadDashboardWithRetry('user_123')

    Without Parameters<Fn>, the wrapper would usually fall back to any[] at the call site or require overloads for every function shape. Parameters lets the wrapper stay generic while preserving the original argument requirements.

    This is one of the most practical utility types for library code because it lets you add behavior around a function without weakening type safety.

    Type class-based APIs with ConstructorParameters and InstanceType

    ConstructorParameters does for class constructors what Parameters does for functions: it extracts the constructor argument tuple. InstanceType takes a constructor type and gives you the type of the object produced by new.

    Together, they are useful in dependency injection containers, plugin registries, factory helpers, and test utilities that accept classes as values.

    // lib/container.ts
    type Constructor<T = unknown> = new (...args: any[]) => T
    
    class Container {
      private instances = new Map<Constructor, unknown>()
    
      register<C extends Constructor>(
        ctor: C,
        ...args: ConstructorParameters<C>
      ): void {
        this.instances.set(ctor, new ctor(...args))
      }
    
      resolve<C extends Constructor>(ctor: C): InstanceType<C> {
        const instance = this.instances.get(ctor)
    
        if (!instance) {
          throw new Error(`${ctor.name} is not registered`)
        }
    
        return instance as InstanceType<C>
      }
    }

    Now the container can preserve constructor arguments and instance types:

    class Logger {
      log(message: string) {
        console.log(message)
      }
    }
    
    class MetricsClient {
      constructor(private apiKey: string) {}
    
      track(eventName: string) {
        // ...
      }
    }
    
    const container = new Container()
    
    container.register(Logger)
    container.register(MetricsClient, 'metrics_key')
    
    const logger = container.resolve(Logger) // Logger
    const metrics = container.resolve(MetricsClient) // MetricsClient

    The benefit is not shorter code. The benefit is that the container no longer has to know every class signature ahead of time while still enforcing constructor arguments when a class is registered.

    Narrow discriminated unions with Extract and Exclude

    Extract<T, U> keeps the members of a union that are assignable to U. Exclude<T, U> keeps the members that are not assignable to U.

    Both are useful with discriminated unions in TypeScript, where you often need a subset of a larger event or state type.

    Consider an application event union:

    // types/events.ts
    export type AppEvent =
      | { type: 'user.signed_in'; userId: string }
      | { type: 'user.signed_out'; userId: string }
      | { type: 'payment.succeeded'; amount: number; userId: string }
      | { type: 'payment.failed'; reason: string; userId: string }
      | { type: 'system.heartbeat' }

    A billing service only cares about payment events. You could define a second union manually:

    Diagram showing TypeScript Extract and Exclude utility types filtering AppEvent into PaymentEvent and AuditableEvent.
    Using Extract and Exclude to derive narrower event types from a broader AppEvent union.
    type PaymentEvent =
      | { type: 'payment.succeeded'; amount: number; userId: string }
      | { type: 'payment.failed'; reason: string; userId: string }

    That works until someone adds payment.refunded to AppEvent and forgets to update PaymentEvent.

    Extract keeps the subset tied to the original union:

    // services/billing.ts
    import type { AppEvent } from '../types/events'
    
    type PaymentEvent = Extract<AppEvent, { type: `payment.${string}` }>

    Now every event whose type starts with payment. is included.

    To make the compiler enforce future cases, add an assertNever helper:

    function assertNever(value: never): never {
      throw new Error(`Unhandled event: ${JSON.stringify(value)}`)
    }
    
    export function handlePayment(event: PaymentEvent) {
      switch (event.type) {
        case 'payment.succeeded':
          return recordRevenue(event.amount, event.userId)
    
        case 'payment.failed':
          return notifyUser(event.userId, event.reason)
    
        default:
          return assertNever(event)
      }
    }

    If someone later adds this event:

    | { type: 'payment.refunded'; amount: number; userId: string }

    PaymentEvent includes it automatically, and the assertNever branch forces the missing case to surface during type checking.

    Exclude handles the inverse problem. To remove system events from an audit log type:

    type AuditableEvent = Exclude<AppEvent, { type: `system.${string}` }>

    Use Extract when you want a subset. Use Exclude when you want everything except a subset.

    Use NonNullable for reusable non-null guards

    NonNullable<T> removes null and undefined from a type.

    In older TypeScript versions, this was especially important for array pipelines because TypeScript did not reliably narrow filtered arrays:

    const parsed = rawRows.map(row => tryParseRow(row)) // (Row | null)[]
    const valid = parsed.filter(row => row !== null)

    In TypeScript ≥v5.5, simple predicates like row => row !== null are inferred more precisely, so valid can narrow to Row[] automatically.

    Even so, NonNullable is still useful when you want a reusable, named guard:

    // utils/isNonNull.ts
    export function isNonNull<T>(value: T): value is NonNullable<T> {
      return value !== null && value !== undefined
    }

    Then use it across pipelines:

    const validRows = rawRows
      .map(row => tryParseRow(row))
      .filter(isNonNull)

    This has three advantages:

    1. It works clearly in codebases that still support older TypeScript versions
    2. It avoids repeating null checks across pipelines
    3. It communicates intent better than a one-off inline predicate

    Be careful with truthiness checks:

    const scores = rawScores.filter(Boolean)

    That removes 0, '', and false, not just null and undefined. When you mean “not nullish,” use an explicit nullish check.

    Use Record for exhaustive lookup objects

    Record<K, V> creates an object type whose keys are K and whose values are V. It overlaps with index signatures, but the distinction matters.

    A string index signature accepts any string key:

    type Handlers = {
      [key: string]: (event: AppEvent) => void
    }

    That is flexible, but it does not guarantee that every known event has a handler.

    A Record with a finite key union does:

    import type { AppEvent } from '../types/events'
    
    type EventType = AppEvent['type']
    
    type EventHandlerMap = {
      [K in EventType]: (event: Extract<AppEvent, { type: K }>) => void
    }
    
    const handlers: EventHandlerMap = {
      'user.signed_in': event => {
        sendWelcomeBackMessage(event.userId)
      },
    
      'user.signed_out': event => {
        clearSession(event.userId)
      },
    
      'payment.succeeded': event => {
        recordRevenue(event.amount, event.userId)
      },
    
      'payment.failed': event => {
        notifyUser(event.userId, event.reason)
      },
    
      'system.heartbeat': event => {
        recordHeartbeat(event.type)
      },
    }

    This mapped type is slightly more verbose than Record<EventType, Handler<AppEvent>>, but it is more precise. Each key gets the correct event variant, so the payment.succeeded handler knows about amount, while the payment.failed handler knows about reason.

    For a deeper explanation of this utility type, see LogRocket’s guide to TypeScript Record types.

    The same pattern works for permissions:

    type Role = 'admin' | 'editor' | 'viewer'
    type Resource = 'posts' | 'comments' | 'settings'
    type Action = 'read' | 'write' | 'delete'
    
    const permissions: Record<Role, Record<Resource, Action[]>> = {
      admin: {
        posts: ['read', 'write', 'delete'],
        comments: ['read', 'write', 'delete'],
        settings: ['read', 'write', 'delete'],
      },
    
      editor: {
        posts: ['read', 'write'],
        comments: ['read', 'write'],
        settings: ['read'],
      },
    
      viewer: {
        posts: ['read'],
        comments: ['read'],
        settings: ['read'],
      },
    }

    If you remove a role or resource, TypeScript points at the missing key. That makes Record useful for configuration that needs to stay exhaustive.

    Table showing admin, editor, and viewer permissions across posts, comments, and settings, with missing entries flagged as TypeScript compile errors.
    A Record-based permissions map can enforce every role and resource combination at compile time.

    One caveat: Record<string, V> is broad, just like a string index signature. The most useful version of Record usually starts with a finite union of keys.

    Use InstanceType when classes are values

    Most of the time, you do not need InstanceType. If you have a User class, you can write User as the instance type and move on.

    InstanceType becomes useful when the class itself is passed around as a value.

    type PluginConstructor = new (...args: any[]) => unknown
    
    class PluginRegistry {
      private plugins = new Map<string, PluginConstructor>()
    
      register<C extends PluginConstructor>(name: string, plugin: C) {
        this.plugins.set(name, plugin)
      }
    
      create<C extends PluginConstructor>(
        plugin: C,
        ...args: ConstructorParameters<C>
      ): InstanceType<C> {
        return new plugin(...args) as InstanceType<C>
      }
    }

    This pattern shows up in:

    • Plugin systems
    • Dependency injection containers
    • Test factories
    • ORM model registries
    • Framework code that instantiates user-provided classes

    The common thread is that the type system needs to understand the relationship between a class constructor and the instance it creates.

    Stop one parameter from widening a generic with NoInfer

    NoInfer<T> tells TypeScript not to use a particular position when inferring a generic. It is useful when one argument should define the generic, while another argument should only be checked against it.

    This comes up in generic APIs where a fallback, default value, or configuration option accidentally widens the inferred type.

    Consider a selector helper:

    // Without NoInfer
    function selectById<T extends { id: string }>(
      items: T[],
      fallback: T,
    ): (id: string) => T {
      return id => items.find(item => item.id === id) ?? fallback
    }
    
    const users = [{ id: '1', name: 'Ada', role: 'admin' as const }]
    
    const getUser = selectById(users, { id: '0', name: 'Guest' })

    Depending on the values involved, TypeScript may infer T from both items and fallback, producing a wider type than intended.

    NoInfer pins inference to items:

    function selectById<T extends { id: string }>(
      items: T[],
      fallback: NoInfer<T>,
    ): (id: string) => T {
      return id => items.find(item => item.id === id) ?? fallback
    }

    Now T is inferred from items, and fallback must be assignable to that already-decided type. If it is not, the error points at the fallback, which is where the mismatch actually lives.

    NoInfer is available in TypeScript ≥v5.4. If your project uses an older TypeScript version, this type will not be available globally.

    This is a more advanced generic pattern. For related concepts, see LogRocket’s guide to understanding infer in TypeScript.

    Generate type-safe names with intrinsic string manipulation types

    TypeScript includes four intrinsic string manipulation types:

    These types transform string literal types at compile time:

    Utility type Example Result
    Uppercase<T> Uppercase<'admin'> 'ADMIN'
    Lowercase<T> Lowercase<'ADMIN'> 'admin'
    Capitalize<T> Capitalize<'email'> 'Email'
    Uncapitalize<T> Uncapitalize<'Email'> 'email'

    These types only transform string literal types at compile time. They do not change runtime strings.

    They become useful when combined with mapped types and template literal types. For example, you can generate setter names from object keys:

    // hooks/useFields.ts
    type Setters<T> = {
      [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void
    }
    
    function useFields<T extends Record<string, unknown>>(
      initial: T,
    ): [T, Setters<T>] {
      // Implementation omitted
      return [initial, {} as Setters<T>]
    }
    
    const [fields, setters] = useFields({
      name: '',
      email: '',
      age: 0,
    })
    
    setters.setName('Ada')
    setters.setEmail('ada@example.com')
    setters.setAge(36)

    The setter names are derived from the input keys:

    • name becomes setName
    • email becomes setEmail
    • age becomes setAge

    If you rename name to fullName, TypeScript replaces setName with setFullName at the type level. Any stale call sites fail during type checking.

    This pattern also works for event handler props:

    type EventName = 'click' | 'focus' | 'blur'
    
    type EventHandlers = {
      [K in EventName as `on${Capitalize<K>}`]: () => void
    }

    That produces:

    type EventHandlers = {
      onClick: () => void
      onFocus: () => void
      onBlur: () => void
    }

    For more examples of type-level transformations, see LogRocket’s guide to TypeScript mapped types.

    When to use these utility types

    Utility types are most helpful when they prevent drift. They are less helpful when they hide a simple shape behind a complicated type expression.

    Use this table as a quick decision guide:

    Use this pattern When it helps When to avoid it
    Awaited<ReturnType<typeof fn>> The function is the source of truth The type is a public API contract
    Parameters<typeof fn> You are writing a wrapper around a function A simple explicit signature is clearer
    ConstructorParameters<T> You accept classes as values You are instantiating a known class directly
    Extract / Exclude You need subsets of a union The subset is unrelated to the original union
    NonNullable<T> You need reusable nullish guards A simple inline check already narrows clearly
    Record<K, V> You need exhaustive keys from a union The key set is truly open-ended
    InstanceType<T> You need the instance from a constructor type You already have a concrete class type
    NoInfer<T> One argument should not influence generic inference The generic is easy to infer from all arguments
    String manipulation types Names follow a predictable convention Runtime strings need actual transformation

    Conclusion

    TypeScript utility types are most valuable when they make the compiler enforce relationships that already exist in your code. If a component depends on a loader’s return shape, derive that type with Awaited and ReturnType. If a wrapper should preserve a function’s arguments, use Parameters. If an event handler map should cover every event variant, use a finite union with Record, Extract, or a mapped type.

    The common theme is drift prevention. Utility types reduce the gap between runtime code and type declarations, which is where many TypeScript bugs begin. They are especially useful in application code that changes often: API clients, route loaders, state machines, plugin registries, permission maps, event systems, and generic helper functions.

    They also come with a readability cost. A plain interface is often better when the shape is stable, public, and easier to understand directly. A nested utility type is better when the alternative is duplicating a type that will eventually fall out of sync. The right question is not “Can I express this with a utility type?” It is “Does this utility type remove a second source of truth?”

    Used with that constraint, utility types make TypeScript codebases easier to evolve. They let you refactor functions, unions, classes, and configuration objects while keeping related types attached to the source of truth. That makes them less about advanced type tricks and more about long-term maintenance.

    The post TypeScript utility types you’re probably underusing appeared first on LogRocket Blog.

    Read the whole story
    alvinashcraft
    1 minute ago
    reply
    Pennsylvania, USA
    Share this story
    Delete
    Next Page of Stories