The underlying API for the case management system has a quirk. When you send it a request with an invalid or expired authentication token, it does not return HTTP 401 (Unauthorized) or HTTP 403 (Forbidden). It returns HTTP 200 with a JSON body containing {"errors": [{"errorMessage": "You must be logged in to perform that action."}]}. The HTTP layer says "fine, here's your response." The body says "no."
This is bad API design — HTTP status codes exist for exactly this purpose, and code-level res.ok checks assume they work. But the API is the API. Every caller has to inspect the body, not just the status.
Earlier in this project, a release shipped that closed this gap inside the audit log. Writes that the API rejected with this pattern had been silently dropped from the audit trail because the rejection-detection code ran AFTER the status-only check. The fix was straightforward: move the audit call before the status check, parse the body, look for the errors array, log the rejection with the actual error message preserved.
That release closed one site. There were four more.
The first one I knew was there
Days later, during a regression pass, I ran aaicase --check with an intentionally bad token in the environment. The expected behavior was a clear failure: token rejected, exit 1, error message naming the problem.
The actual behavior was a clean pass: "✓ Merus API: connected" followed by the other check items. The exit code was 0.
The code in question probed /users/index and looked at healthRes.ok. ok is true for any HTTP 2xx, including the API's 200-with-errors-body pattern. The check was lying when the token was bad.
This was the SAME bug. Same API quirk, same code shape, same wrong assumption. The audit log had been fixed weeks ago. --check had been written before that fix and never updated. I had fixed the instance and not the shape.
The fix for --check was the same as the audit-log fix from weeks earlier: parse the body, detect body.errors as failure, detect body.data as success, report unexpected shapes with a diagnostic. Exit 1 on failure, 0 only on confirmed connectivity. The output now reads ✗ Merus API: API rejected: You must be logged in to perform that action.
Then I did what I should have done weeks earlier: I grepped.
Three more
The codebase has roughly fifteen call sites that issue an HTTP request to the API and look at the response. Most of them go through a shared helper that does the body-inspection correctly (the helper was the original site that got the fix). Four of them did not.
The dashboard status line. When you type /dashboard or ? in a running session, the response starts with a status block: "Merus: connected" or "Merus: error (HTTP N)". The probe used the same healthRes.ok pattern. With a rotated token, it said "connected" cheerfully. The fix mirrored --check: parse the body, branch on errors versus data, label the dashboard status with the actual rejection message if applicable.
The setup wizard's token-test. When you run aaicase --setup to configure a new install, the wizard asks for your API token, writes it to the config file, then verifies the token works by hitting /users/index. The check was if (!testRes.ok). With a bad token, the wizard saw 200 and reported "Connected to Merus. Starting setup wizard..." It then proceeded to save a config file with a broken token, leaving the attorney with a setup that wouldn't actually work the next time they ran the tool. The fix here was the most consequential of the four — first-run setup wizards are exactly where the body-inspection matters most, because there's no other validation path to catch the bad config.
The events helper. The merus-events binary fetches the firm's schedule. With a bad token, the response had no data field, so the code path printed "No events data" and exited 0. To an attorney looking at the output, this is indistinguishable from "your firm has no events scheduled today" — they'd close the terminal and move on without knowing that their schedule view had silently broken. The fix now detects the errors array and surfaces "Merus API rejected the request:
The search helper. The merus-search binary, called for every case lookup, had a different version of the same gap. With a bad token, the response had no data field, so the code printed "Unexpected response format — no data field" and exited 1. Right exit code, wrong error message. An attorney seeing "unexpected response format" investigates the response shape; the actual cause is the auth failure they need to fix. Now the error explicitly reads "Merus API rejected the request: You must be logged in to perform that action."
Four sites, one underlying pattern. Each fix was small. None of the production flows had been broken in a visible way — the worst case had been "everything looks fine until you go looking," which is a class of failure that's especially hard to notice because everything LOOKS fine.
What the grep should have been
The audit-log fix that landed weeks before should have included a grep pass. Looking back, I think the implicit assumption was that the audit log was special — the audit-log fix was about audit trail correctness, and the other call sites were about user-facing UX, and these felt like different problems. They were not. They were all the same problem: trusting res.ok when the API returns HTTP 200 with a body-level error.
The grep I ran later was effectively res.ok|response.ok|.ok, which is brittle (false positives from opts.ok, x.ok meaning unrelated things), but caught everything in this codebase. Fifteen call sites total. Eleven went through the shared helper and were correct. Four were stand-alone code that re-implemented the HTTP loop and reproduced the bad assumption.
The discipline now is: when a fix is for a pattern in the API's behavior (not a single buggy code path), find every place the pattern shows up. Grep is cheap. Re-discovering the same bug six weeks later in a different code path is not cheap — the system might have been quietly wrong for that whole stretch, and the attorneys who depended on the system might have been making decisions based on stale signals.
The four releases
The fixes shipped as 4.5.45 (--check), 4.5.46 (dashboard + setup + events + search bundled), and 4.5.47 (a small follow-up where a script-callable command was returning exit 0 on a not-found condition — same family of "treating wrong as right" but at a different layer). Two more shipped after that as additional regression turned up edge cases — one for the merus-pdf page-count parser falling back to a heuristic that returned 0 for some real PDFs, one for the install script not pulling Poppler on Mac. Each release was small. Each one closed a specific instance.
What I learned to write down somewhere I can find it: the pattern HTTP 200 + errors body = failure is now part of the project's working knowledge. Every new HTTP call site that gets added has to handle it, because the API is going to keep doing this and our defenses against it are case-by-case until they're shared. The shared helper covers most of them. The exceptions need their own audit pass each time something like --check gets written.
The version on the install URL is 4.5.56. The work continues at the pace of one verified release at a time, and a fair chunk of that work has been hunting the same bug across the call sites that quietly reproduced it. Once you know the shape, you can find it everywhere it lives. The shape has been found and named. The next call site that reproduces it should be easier to spot.