---
title: "Three compounding bugs spawned a burst of near-identical tickets, and `tail -15` hid the evidence"
canonical: https://dxdev.com/blog/tail-15-hid-the-success-line-and-caused-an-infinite-retry/
datePublished: 2026-05-18
---
Four near-identical JIRA tickets showed up seconds apart, with no human in the loop. My first guess was a flaky board or a double-submit somewhere. It wasn't either. One automated session had fired my ticket-creation command seven times inside two minutes, and the reason it kept firing is the dumbest thing in this whole writeup: a `tail -15` at the end of the pipeline ate the success line, so the agent driving the command could never tell that the call it just made had already worked.

This is a postmortem on three small bugs that stacked into one loud failure. The top bug is the transferable one. The other two are the kind of platform-specific papercuts that make the first bug fatal instead of harmless.

## The trace

The mess came out of a single session that ran `bhq site-new` (my "create a new ticket" command) seven times between 01:06 and 01:08. The output of that burst was four near-identical tickets. Two were the real work the session was supposed to file. The other two were duplicates, each one a retry of the call right before it. Later, while building the fix, I created one more dupe as a smoke test, so the final cleanup deleted three tickets, not two.

So: two intended creates, run as seven calls, left two duplicates behind in the burst. Nobody asked it to retry. It retried because it genuinely could not see that the call it had just made had succeeded. Here is why.

## Bug 1: the pipeline truncated its own success line

Every invocation of the command was wrapped like this, to keep the captured output small:

```
bhq site-new "(Page) Title" ... 2>&1 | tail -15
```

`tail -15` is a reasonable instinct. Command output can be noisy, you only want the tail, you cap it at the last 15 lines and move on. The problem is that `bhq site-new` prints about 18 lines on success, and the one line that actually matters, the `CREATED: ITEM-####` confirmation, is line one. It's a header. Line two is the ticket URL, line three is blank, and only from line four on does the body start: a `--- set ---` block of field echoes (type, epic link, Features label, the rendered fields).

So `tail -15` did exactly what it was told. It threw away the first three lines, including the `CREATED:` header and the URL right under it, and handed back the last fifteen. The fifteen lines it kept started at that `--- set ---` block, a bunch of field echoes with no visible success marker. From the agent's point of view, the command had produced output but no confirmation that the ticket was created. The only safe-looking move when a create command appears to have run without confirming success is to run it again. And again.

That is the whole trap, and it's worth saying plainly: a pipeline that truncates a tool's success line turns a working call into a retry loop. The call worked. The evidence that it worked got cut off by the pipe. Truncation that drops the header but keeps the body inverts the meaning of the output, from "succeeded" to "no idea." On a command with no other side-effect guard, "no idea" plus a retry policy equals duplicates.

## Bug 2: MSYS rewrote the argument before the script ever saw it

The second bug is why one of the dupes was not just a duplicate but a malformed one. The first real attempt was passed `--page "/staff-admin/"`. On Windows, running through git-bash, MSYS's automatic POSIX-to-Windows path translation looked at that leading-slash argument, decided it was a path, and rewrote it before the Python script ever read it. `/staff-admin/` came out the other side as `C:/Program Files/Git/staff-admin/`, the embedded space split the token, and the Features field got written as two garbage values.

A mangled `--page` value corrupts Features-label resolution, which is how the ticket gets homed to the right epic. The script didn't catch it because the call also passed an explicit `--epic`, and supplying one skips the one code path (`resolve_epic()`) that would have validated the Features label. So the call wrote a ticket with a corrupted routing field, and Bug 1 then made it look like that call had halted, so the agent retried with a cleaned-up `--page "StaffCenter"` and got the first real ticket. This is the git-bash footgun that bites anything with a `/`-leading argument: MSYS will silently rewrite it, and your script gets a string you never typed.

## Bug 3: a shell-snapshot init error made a good call look failed

The third bug pushed the loop along from the other side. One of the calls (the fifth, at 01:07:02) hit a Claude Code shell-snapshot init error, the classic `exit 127 / No such file or directory` from a broken snapshot wrapper. The inner `eval` actually ran and created the second duplicate, but the Bash tool surfaced exit 127 as if the whole thing had failed. So on that call you had two independent signals lying in the same direction: `tail -15` hid the success header, and a nonzero exit code from the shell init said "this failed." A call that succeeded looked failed on both the output channel and the exit channel, and the agent retried it into the second real ticket. Retrying was, given that evidence, the correct decision. The evidence was just wrong.

## Why three bugs and not one

None of these alone produces a burst of duplicates. If `bhq site-new` had been idempotent, the truncated output wouldn't matter, the retries would no-op. If the exit code had been honest, the agent would have trusted success on the call that hit it. If MSYS hadn't mangled `--page`, at least the first dup would have been clean enough to reason about. It took the three stacking: a create command with no dedup guard, a pipeline that hid the one line proving success, and an exit code that affirmatively claimed failure on a call that worked. Each one made the others lethal.

## The hardening

The fix was three guards, and I made a point of confirming the first two actually fired before I declared anything done.

**Move the success header to the last line.** `bhq site-new` now repeats `CREATED: <key>` plus the ticket URL as the last line of its output, not the first. Now `| tail -N` callers physically cannot lose the confirmation, no matter how aggressive the truncation. This is the cheap, universal one: if any caller might pipe your command through `tail`, your success marker belongs at the bottom.

**A 5-minute duplicate guard.** A new `find_recent_duplicate()` check halts the create if a ticket with the same summary and the same Features label was created in the last 5 minutes. An explicit `--allow-recent-duplicate` overrides it when you really do mean to file twins. This is the backstop for the case where the success line is still somehow lost: even a blind retry loop can only make one ticket per 5-minute window now.

**A page-label assert before the POST.** `assert_page_label_known()` halts before hitting JIRA when `--page` resolves to a bogus Features label, which catches the MSYS mangling specifically, even when an `--epic` is also supplied. Bad routing field, no POST.

Both the 5-minute window and the page guard were proven to fire during cleanup, not just written and hoped at.

One cleanup wrinkle worth knowing: PREPARING-status tickets offer no "close as duplicate" workflow transition, so I couldn't resolve the dupes the polite way. The three throwaways (the two burst duplicates and the smoke-test dupe) got deleted straight through the JIRA REST API, `curl -X DELETE .../rest/api/2/issue/<key>`, leaving the two real tickets as the pair. The hardening all landed under one internal ticket.

## The takeaway

Never blindly pipe a command through `tail` when you depend on its confirmation line, and if you write commands that other tools consume, put the success header at the end of the output, not the start. A header-first success line is fine for a human reading a full terminal. It's a landmine for any caller that truncates, because truncation that keeps the body and drops the header doesn't just lose information, it flips the meaning from "this worked" to "this is unconfirmed," and "unconfirmed" is what triggers the retry.

And the broader version: when an automated caller does something destructive or duplicative on retry, don't only fix the retry logic. Fix the signal it retried on. The agent here behaved correctly given the evidence it had. The evidence was a truncated stream and a lying exit code. Make the success unmissable, make the operation idempotent within a window, and validate the args your shell might have rewritten behind your back. Then a single lost confirmation line costs you nothing instead of a board full of duplicates.

## Related

- [The stale credential that failed two different code paths at once](stale-credential-failed-two-code-paths-at-once): another postmortem where one broken input produced multiple independent-looking failures
- [The runtime was half dead: use a migration as the audit you'd never run](the-runtime-was-half-dead-migration-as-audit): using a forced migration to surface hidden broken state before it causes cascades
- [My red CI was a lie: a deleted workflow haunted every push while nothing real ran](ghost-workflow-zero-second-failure-masked-no-ci-running): a different "the signal was lying" failure where CI status reported wrong
- [UTC logs, a local clock, and the canary request: timezone discipline in an incident](canary-request-utc-logs-incident-timing): observability discipline for accurately reading what a pipeline did and when
- [The "queue worker drains it" story was vapor: the create command said NOT YET IMPLEMENTED](queue-worker-was-never-built-verify-the-effect-not-the-trigger): verifying the effect, not just the trigger, the same "assume success and be wrong" pattern
