For a few days, Influxx on Windows was quietly spawning a new PowerShell process roughly twice a second, on every machine running it, whether or not anyone was touching a credential. The app didn't crash. It just got slower and slower until unrelated operations started timing out. The root cause was a single word: a caching strategy that looked correct on paper and never once produced a cache hit in practice.
Why Windows Won't Let Node Just Chmod a File
Influxx stores credentials for git providers and coding-agent CLIs in a local file-based store on disk. On macOS and Linux, keeping that store locked down to the current user is straightforward: set restrictive file permissions with a standard POSIX mode call and the filesystem enforces it. On Windows, that call is a documented no-op. Node's file-write permission flag does not translate to anything meaningful on NTFS — the permission bits it accepts describe a POSIX model that Windows' access-control system doesn't use, so the call silently succeeds without changing anything about who can read the file.
Real access control on NTFS means access-control lists (ACLs): explicit entries naming which users and groups get which rights, plus inheritance rules that determine what a new file picks up from its parent directory by default. Node has no built-in API for touching that model. The only practical way to strip an inherited, overly broad permission set from a credential file on Windows is to shell out to PowerShell and run its access-control-list cmdlets directly against the file or directory.
That's not a workaround Influxx chose lightly — it's the actual mechanism Windows requires. But it comes with a real cost that doesn't exist on the other two platforms Influxx ships to, and that cost is what turned a correctness fix into a performance incident.
The Hidden Cost of Shelling Out to PowerShell
Every PowerShell invocation is a fresh process. On a typical developer machine, a cold PowerShell start costs somewhere between 1 and 1.5 seconds before it even runs the ACL command you asked for. That's not a network round trip or a disk read that can be optimized away with a faster drive — it's the interpreter itself spinning up: loading the .NET runtime, initializing the PowerShell host, parsing profile scripts if any are configured, and only then executing the one line of code that actually matters.
A second and a half is invisible if it happens once, at app startup, while the credential store is first created. It is very much not invisible if it happens on a hot path that runs multiple times per second. The gap between those two situations — one PowerShell call at launch versus dozens per minute during normal use — is exactly the gap Influxx fell into.
How a Read Path Turned Into a Spawn Storm
The bug traces back to a credential-store read function that called the file-hardening routine on every single read, not just on writes. That was defensible in isolation: if you're about to read a secret off disk, checking that its permissions are still locked down is a reasonable safety habit. The problem is that "hardening" on Windows means "shell out to PowerShell," and nobody had audited how often that read function actually got called.
The answer used to be: rarely. Then an unrelated change increased how often the credential store was polled, to roughly twice a second, to keep provider connection status feeling responsive elsewhere in the app. Nothing about that polling change touched the hardening logic directly. But it multiplied the read function's call frequency by an order of magnitude, and the read function was still calling PowerShell's ACL cmdlets on every invocation. Two polls a second times a 1-to-1.5-second PowerShell cold start is a formula for a process that can never catch up with itself.
The Cache That Never Cached
The first attempt at a fix was a cache keyed on the credential store directory's last-modified timestamp: if the directory's mtime hadn't changed since the last hardening pass, skip PowerShell and trust the cached result. It's a reasonable-sounding invariant — nothing changed, so nothing needs re-securing — and it would have worked if the directory's timestamp were stable between hardening passes.
It wasn't. Every secure write to a file inside that directory updates the directory's own last-modified timestamp as a side effect of the filesystem doing its normal bookkeeping. Which means the very act of hardening a file — the operation the cache existed to gate — invalidated the cache key that was supposed to prevent redundant hardening. The cache compared the current mtime against a stored value that was, by construction, almost never equal to it. Every read triggered a "cache miss," which triggered a fresh PowerShell spawn, which triggered a directory write, which changed the mtime again before the next read arrived.
- Poll frequency: the credential store was being read roughly twice a second after the unrelated polling change shipped.
- Spawn rate at peak: the broken mtime cache let PowerShell spawns climb to roughly 1.8 per second — essentially one new PowerShell process for nearly every poll.
- Cost per spawn: each of those processes cost 1 to 1.5 seconds of cold-start time before doing any real work.
Multiply those together and the app's main thread was effectively saturated with PowerShell process creation. Anything else competing for that thread — UI updates, other file operations, background checks with their own timeouts — started missing its deadlines. The symptom that got reported wasn't "credential hardening is slow," it was "the app freezes and unrelated things start timing out," because from the outside, a thread pinned by repeated process spawns looks exactly like general unresponsiveness.
"The mtime cache passed code review because the logic reads as correct — compare a timestamp, skip the expensive call if nothing changed. What we missed is that the expensive call itself was one of the things changing the timestamp. It's a self-invalidating cache, and those are nasty because they don't fail loudly, they just fail to ever help."
— Priya Nandakumar, Staff Engineer, Desktop Platform at ETAPX
Three Paths, Three Strategies
The actual fix isn't one caching strategy — it's three, chosen per code path based on how often that path runs and how much a stale result would actually cost if it were wrong. Treating "harden this file's permissions" as a single operation with a single caching rule was the original mistake; different callers have genuinely different security and performance requirements, and the cache has to reflect that.
Directories: Async, Cached for the Process Lifetime
Directory permissions don't need to be re-verified on every access — once a directory's ACLs are locked down for the running process, there's no realistic scenario where they silently regress mid-session without something else going seriously wrong first. So directory hardening now runs asynchronously and fire-and-forget, and the result is cached by path for the lifetime of the running process. The first access to a given directory pays the PowerShell cost once; every subsequent access in that process hits the cache and pays nothing.
Writes: Synchronous, Every Time
The write path is the one place where speed loses to correctness on purpose. Credential files are hardened synchronously before the write function returns, which closes a real window: without it, a freshly written credential file could briefly sit on disk under an overly broad inherited permission set before hardening caught up asynchronously. That window might only be open for a fraction of a second, but it's a window during which a secret is more exposed than the design intends, and no caching strategy is allowed to reopen it. Writes are comparatively rare, so paying the full PowerShell cost synchronously on every one of them is an acceptable trade.
The write-path cache also has a specific correctness rule: a path is only marked "already hardened" after a confirmed successful permission change. If the PowerShell call fails, nothing gets cached as safe, and the very next write retries hardening from scratch. Caching an unconfirmed or failed attempt as success would have quietly reintroduced the same exposure the synchronous write was built to prevent.
Reads of Existing Files: Async, At Most Once Per Run, Keyed on Identity
This is the path that caused the incident, and it's the one that needed the most careful key design. Existing files on the read path are re-hardened asynchronously, but at most once per process run — and the cache key isn't a timestamp on a parent directory, it's the file's own identity: inode, size, and timestamps taken together. That combination survives a rename. If a credential file gets moved or renamed, its identity tuple stays recognizable, so it's still correctly detected and re-hardened exactly once rather than being treated as a brand-new, never-hardened file, or worse, silently skipped because a naive path-based cache thought it had already seen that filename.
- Directories: async, fire-and-forget, cached by path for the process's entire lifetime.
- Writes: synchronous, every call, cache only set on confirmed success.
- Reads: async, at most once per process run, keyed on file identity rather than a mutable directory timestamp.
"I noticed it because my terminal panes would just stutter every couple of seconds, almost rhythmically, and I assumed it was my machine or something in my agent config. Once the fix shipped it was like someone turned off a background vibration I didn't fully realize was there the whole time."
— Marcus Webb, DevOps lead at a logistics software company, Influxx user
Why the Test Suite Never Caught This
The most uncomfortable part of this story is that Influxx's automated end-to-end test suite runs on Linux, where the permission-hardening routine short-circuits to a simple chmod call and never touches the PowerShell code path at all. That's not a gap anyone introduced carelessly — running CI on Linux is faster and cheaper, and chmod genuinely is the correct, working implementation on that platform. But it means this entire class of regression is structurally invisible to automated testing as currently configured. A broken Windows-only cache can pass every CI run cleanly and still saturate a user's main thread in production.
The only way to catch this class of bug is a manual Windows test plan that specifically counts live PowerShell process spawns before and after a change to the hardening logic — not just "does the credential still get stored," but "how many PowerShell processes did that operation actually create, and does that number scale with usage in a way that would be a problem at real polling frequencies." That's a narrower, more deliberate check than a typical pass/fail integration test, and it only exists because someone had to go looking for it after the fact.
"Cross-platform desktop software has these seams where a platform API just isn't there, and you end up building a real subsystem to cover for it. Those seams are exactly where regressions hide from CI, because your automated coverage is quietly running on the platform where the seam doesn't exist. We're treating manual, platform-specific verification as a permanent part of how this code gets reviewed, not a one-time cleanup after an incident."
— Sana Okafor, VP of Engineering at ETAPX
What This Means If You're Building on Windows
The general lesson here generalizes past credential files. Any time a security-relevant operation on Windows has to go through a subprocess because the platform API you'd use elsewhere is a no-op, that operation's true cost is not the cost of the operation itself — it's the cost of the subprocess, and it needs to be budgeted against however often the calling code actually runs, not however often you assume it runs when you write the caching layer. A cache that looks correct in a code review can still be wrong if the value it's keyed on is mutated by the very operation it's meant to prevent. And a caching decision made for one call site doesn't transfer safely to another call site with different frequency and different consequences for staleness — which is exactly why the fix here ended up being three separate strategies instead of one shared one.
For anyone evaluating Influxx specifically, the practical upshot is that credential storage on Windows is now fast in the paths that need to be fast — directory checks are effectively free after the first one, reads pay a hardening cost at most once per run — while staying strict in the one path where strictness matters most: a credential is never written to disk and left under loose permissions, even momentarily, because that hardening call is synchronous and blocking by design.
Frequently Asked Questions
Does this affect credential storage on macOS or Linux?
No. Standard file-permission calls work correctly on those platforms' filesystems, so the PowerShell subprocess path and its caching logic are Windows-specific; macOS and Linux never spawn a subprocess for this operation.
Why not just harden the directory once at app startup and skip per-file checks entirely?
Directory-level hardening is in fact cached for the whole process lifetime, but individual credential files still need their own read-path and write-path checks because a file's permissions can diverge from its parent directory's, particularly around inheritance when a file is first created.
Could a failed PowerShell hardening call leave a credential file exposed?
The write-path cache only marks a file as hardened after a confirmed successful permission change; a failure is never cached as success, so the very next write retries hardening rather than silently trusting an unconfirmed state.
What happens if a credential file gets renamed on disk?
The read-path cache keys on file identity — inode, size, and timestamps together — rather than the file's path or name, so a renamed file is still correctly recognized and re-hardened rather than either being skipped or unnecessarily re-processed as if it were untouched.
How was the 1.8 spawns-per-second figure measured?
It came from manually counting live PowerShell process creation on Windows during the incident, at the credential store's roughly twice-per-second poll rate, since the automated test suite runs on Linux and never exercises the PowerShell code path at all.
Why does the read path only re-harden once per run instead of caching forever?
A permanent cache would miss a file whose permissions were changed outside the app during that same run, so re-hardening once per process start balances catching that scenario against not repeating an expensive PowerShell call for a file that hasn't moved.
Is the underlying problem specific to Influxx's credential store, or a general Windows/Node limitation?
It's a general limitation — Node's standard permission APIs don't map to Windows' access-control-list model at all — so any Node application enforcing real file permissions on Windows has to shell out to something like PowerShell's ACL cmdlets, and needs a caching strategy at least this deliberate to avoid the same failure mode.
The bug was never that hardening credential files was too slow — a second and a half at startup is nothing. It was that one cache pretended a mutating operation left its own invalidation key untouched, and the fix was to stop treating "cache the hardening result" as one problem with one answer, instead of three call sites with three different tolerances for staleness.

