Skip to content
Harness EngineeringPart 5 of 156 min read

Tickets In, Reports Out: Giving AI Agents Safe Access to a Client's Issue Tracker

QS
Quanta Studio Engineering
October 15, 2026
The tracker sat behind the client's VPN and its tickets contained personal data. How we built a redacted local mirror agents could read, the triage loop on top of it, and a monthly client report generated from tickets and commits.

In the last two months of this project, most of our work arrived as tickets from the client's QA and backend teams. We wanted agents to read those tickets directly. Two things stood in the way: the tracker sat behind the client's VPN, and tickets contain names, phone numbers and test credentials.

Here is how we gave agents tickets without giving them personal data.

A redacted local mirror

One command runs the whole thing as a short-lived Docker Compose job:

  1. Connect. A VPN client container brings up the tunnel. Its health check probes the tracker through the tunnel, not just the network interface; a tunnel once sat "up" but dead for four weeks behind an interface-only check.
  2. Fetch. A Playwright scraper shares that container's network. It logs in, exports the ticket list, works out what has changed and fetches only those tickets, with page loads rate-limited.
  3. Redact. People become "Reporter" and "Member 1". Tokens, JWTs, phone numbers, ID numbers, card-like numbers and IP addresses are masked, and private notes are dropped.
  4. Write. Each ticket becomes a Markdown file with frontmatter for status, tracker, dates and format version, and the index is rebuilt.

Changing a redaction rule bumps the format version, which re-renders every ticket. The mirror has its own test suite, and every commit in its repository was co-authored by the agent. It started inside one app repository and moved to the shared parent folder a day later, once it was clear all three apps needed it.

Redaction by pattern will always miss something, so every repository's instructions carry the same rule: read a ticket before copying any of it into a repository, pull request or chat. Nobody hand-edits a ticket file. A missed pattern gets fixed in the redaction code, with a test.

Fetching one ticket by number

Need Help Implementing This?

Our team can help you put these ideas into action. Get in touch for a free consultation.

A later addition let the agent fetch named tickets on demand. The session that built it took about seven minutes:

  • Tests first. "All 14 new tests fail as expected."
  • The new rules. Named tickets bypass the "closed tickets are never backfilled" rule and the per-run cap, but they still go through redaction and the request delay.
  • Checking the entry script. It was tested under the old bash that ships with macOS, with a stub docker command on the PATH. To confirm a setting reached the container, the agent printed only that one key, "so no credentials show up."
  • Saying what it didn't check. The report ended with "Not checked: a run against the real tracker", because that needs the VPN.

Two weeks later this small feature is what made the monthly report below possible.

The triage loop

Ticket work settled into a fixed loop:

  1. Re-fetch the ticket and read it to the last note. Notes get edited in place, so compare against the previous copy.
  2. Read the relevant memory note.
  3. Check the live API (Part 6) or the device (Part 7).
  4. Report the root cause first.
  5. Wait for "go".
  6. Fix it in a worktree, starting with a failing test. The commit cites the ticket.
  7. Open a pull request. A human merges.
  8. Draft the reply to the ticket in a scratch file. The developer posts it.
  9. Update the API spec with the date it was verified live, and move the memory note to the Archive tier.

The correction that tightened it

Did you read the latest notes? Why read an unrelated ticket? Check your own work instead of assuming.

The agent had triaged from a stale copy of a ticket and drifted into a neighbouring one. It re-fetched the ticket, withdrew its conclusion and listed what it had got wrong. Then it saved a rule: re-fetch before triage, stay on the ticket, and make every statement either checked against its source or labelled as the agent's own reading.

Two other corrections shaped the loop:

  • "Could not reproduce" is not a reply. The agent once drafted that answer to QA. The developer reproduced the bug with an exact sequence of taps, and there were two root causes: a duplicate navigator stacked on every jump, and full-resolution decoding of very large images. On a budget phone, memory use on that screen dropped from about 800 MB to about 370 MB. The draft reply was withdrawn.
  • Stop when the goal is met. Late in a long back-and-forth with the backend team, the agent was polishing a third reply draft after the problem had already been solved. The developer asked: "What are those replies for? What problem are you solving?" Now the bottom line comes first, and the work stops when the goal is met.

API changes come in, too

The client's backend team publishes API changes as a repository of request collections. A custom slash command compares that repository with a stored "last integrated commit". It maps each changed endpoint through the app's API layers and writes an integration plan.

It never edits code, and on its first run it refused to guess a baseline and asked for one. Sixteen integration plans have come out of it so far.

Reports going out

Every month the client receives a development log in their own Google Doc format, covering front-end work only. One session produced it in about half an hour:

  • Learn the format. The agent learned the layout from earlier months' PDF exports.
  • Backfill the month. Closed tickets for the month came in through the named-ticket fetch, run in the background with a slower request delay. The first run failed silently. zsh doesn't split a variable into words the way bash does, so every ticket number arrived as one argument, and the agent's own output filter hid the error. It re-ran the fetch through xargs.
  • Classify about 88 tickets against commit messages in the three front-end repositories. 33 had direct evidence (the commit cites the ticket) and 18 matched only by topic. The agent flagged the weakest matches and listed what it left out and why.
  • Find the milestones from first-parent merges into the production branch.
  • Build the document. The agent copied the sample document through the Drive API, which keeps the styling. It edited from the bottom up with the Docs API so positions stayed valid, then exported a PDF and read it back to check.
  • Leave the money to the human. "These are your billing figures, so I did not estimate them." Man-days stayed blank for the developer to fill in.

Getting access was its own lesson. The agent's Google account didn't have the Docs API enabled, so it tried a second Google profile. The permission classifier blocked that as credential exploration, and the agent stopped and asked. The developer chose the account, enabled the API, and the agent continued. Part 11 has more on that boundary.

What to copy

  • Mirror tickets locally with redaction, and still read before you copy.
  • Health-check the path you actually use, not just whether the tunnel is up.
  • Make the agent re-fetch and stay on the ticket, and label any statement it hasn't checked.
  • Let agents draft replies and reports, and keep billing and posting with a human.
  • Turn API changes into a plan before anyone edits code.
#Harness Engineering#AI Agents#Issue Tracking#Privacy#Automation
A NOTE FROM THE STUDIO

Get Weekly Insights

Practical tips on web development, SEO, and growing your business online. Delivered every Thursday.

No spam. Unsubscribe anytime.

Ready to Take Action?

Let's discuss how these strategies can be applied to your specific business goals.