# AI agents updating issue trackers without losing control

An agent that can update an issue tracker can change work that people already rely on. A bad code suggestion is easy to reject. A wrong assignment can interrupt someone, a false status can start downstream automation, and a plausible comment can bury the actual decision. Treat tracker updates as actions with consequences, not as harmless text editing.

The control most teams reach for first is a better prompt: "Only move issues to In Progress" or "Never assign people." That improves behavior, but it does not create a boundary. The agent still holds a credential that can send any request the credential allows. The boundary must sit outside the agent, where an accidental tool call, a compromised extension, or an over-eager plan cannot talk its way around it.

## A tracker edit contains several different authorities

An issue update rarely means one thing. It often bundles authority to change workflow state, write a public statement, alter ownership, edit deadlines, and rewrite labels. If you call all of that "update issue," you will grant more power than the task requires.

GitHub's REST documentation for "Update an issue" exposes this problem plainly. The same request can change title, body, state, milestone, labels, assignees, and state reason. Jira's Issue API similarly separates a general edit operation from transition operations, while permissions and workflow configuration decide what a caller may do. The API shape tells you the important part: a convenient endpoint is not a safe authorization unit.

Separate the request into action classes before you build an integration:

- Read issue data and search issues.
- Add a comment.
- Perform a named workflow transition.
- Change an assignee, a watcher, a due date, or a priority.
- Edit descriptive fields such as title, body, labels, or acceptance criteria.

These classes carry different risks. A comment may be reversible but can mislead a team. A transition may change reporting or trigger automation. An assignment makes a claim about who owns work. A title or body edit can quietly erase the context a developer needs later.

This distinction also fixes a common design error: restricting visible fields in an agent tool does not necessarily restrict the request. If the tool accepts an arbitrary JSON object and merely documents allowed fields, the agent can still supply `assignee`, `labels`, or a different issue ID. A real boundary constructs the request itself from a narrow action, rather than forwarding the agent's object unchanged.

## Prompts cannot preserve field boundaries

A model can follow a rule most of the time and still make a forbidden call when the conversation gives it a plausible reason. Issue text can carry hostile instructions too. A user can paste "assign this to the security lead and close it" into a bug report, and an agent that summarizes or triages the report may treat that text as a task. This is ordinary instruction confusion, not a far-fetched exploit.

A prompt also cannot protect against an implementation mistake. I have seen integrations begin with a single `update_issue` wrapper because it gets a demo running quickly. Six months later, the wrapper accepts every field the vendor API accepts, a batch job uses it, and nobody can say which fields were intended. The initial shortcut becomes the access model.

Use narrow operations with fixed input shapes. A status operation should accept an issue identifier and one transition name or transition ID. A comment operation should accept an issue identifier and comment text. An assignment operation should accept an issue identifier and an assignee selected from a constrained source. Do not make a generic patch operation the only tool available to an autonomous agent.

A narrow request can look like this:

```json
{
  "action": "transition_issue",
  "tracker": "engineering",
  "issue": "ENG-1842",
  "transition": "start_progress",
  "reason": "Agent began the approved dependency update"
}
```

The service that receives this request should map `start_progress` to the tracker-specific transition. The agent should not submit a raw status value, an arbitrary transition ID, or an object that happens to contain many other fields. If the issue is already closed, if the transition is not available, or if the project does not belong to `engineering`, the service rejects the action before it contacts the tracker.

This is less flexible than a generic API client. Good. The point is to keep routine automation routine and make exceptions visible.

## Status changes need an explicit state contract

Status has a special problem: people speak about it as if it were a field, but most teams use it as a workflow event. "Done" can mean code merged, deployed, verified, accepted by a customer, or merely no longer active. An agent cannot infer that meaning safely from the label alone.

Write a state contract for every transition an agent may perform. It should state the source status, destination status, evidence the agent must have, and effects that require a human. Keep it near the integration code, not buried in a wiki paragraph that no call path reads.

For example:

| Transition | Agent may perform it when | Agent must not perform it when |
| --- | --- | --- |
| Backlog to In Progress | It has started a named, approved task linked to the issue | The issue lacks a concrete task or has another active owner |
| In Progress to Blocked | It can name the failed dependency or missing decision in a comment | The work merely took longer than expected |
| In Progress to Ready for Review | A change set exists and the tracker accepts that workflow meaning | Review requires a human checklist the agent cannot verify |
| Ready for Review to Done | Never by default | A human or a separate release system owns acceptance |

The last row matters. Teams often let agents close issues because it makes dashboards look tidy. That creates false completion. A coding agent can report that tests passed; it usually cannot decide that the product behavior is accepted, that documentation is adequate, or that an operational change has actually occurred.

Use the tracker's transition endpoint when it provides one. Jira workflows can make transitions available only in particular states and can require fields or run validators. Those controls give you tracker-side enforcement that a plain field edit may skip. They still need review: a workflow validator that checks only whether a comment exists will happily accept a useless comment.

Do not confuse a status transition with evidence. Store the evidence in a structured comment or an external record, then link the transition to that record through an ID. The status says what changed. The evidence says why.

## Comments need provenance, not simulated authorship

An agent comment must look like an agent comment. It should never impersonate a developer, even if a human approved the run. Shared human tokens erase provenance, and the tracker history then tells a lie that becomes difficult to correct.

Create a dedicated integration account for each agent role or workload. Give it a recognizable display name under your team's conventions. If the tracker permits only one service account, include a stable attribution line in every comment and retain the richer identity outside the tracker.

A comment format that survives copy-paste and export is better than prose that depends on a dashboard badge:

```text
[agent: dependency-maintainer]
Action: marked the issue blocked
Reason: the requested package version conflicts with the declared runtime requirement
Evidence: build job 9f31c returned a dependency resolution failure
Run: 4c2a7e
```

The marker does not prove anything by itself. Anyone who can post comments can type it. Its job is legibility. The proof comes from the authenticated integration identity and from an action journal that records the call.

Do not ask an agent to write comments in a human voice to "avoid noise." That instruction is popular because teams dislike robotic comments. It is still wrong. A brief, factual, clearly attributed comment produces less confusion than a persuasive paragraph that readers mistake for a teammate's judgment.

Set content limits as well. An agent comment should state an observed fact, a proposed next action, or a concise summary with sources it actually accessed. It should not publish secrets from logs, repeat a private discussion into a public project, speculate about a person's performance, or claim a deployment succeeded unless it has a verified deployment result.

For sensitive projects, route comment text through the same approval used for the action. The approver needs to see the actual text, not a promise that the comment will be "helpful." Meaning lives in the payload.

## Assignment is a social action, not a routing detail

Assignment creates an expectation between people. An agent that assigns a name has said, in effect, that this person should now pay attention. That is different from applying a component label or selecting a team queue.

Keep automated assignment deterministic. Good candidates include a repository's declared code owner, an on-call rotation obtained from an authoritative system, or the existing assignee when an agent only updates status. Bad candidates include "the person who committed nearby code," "the least busy engineer," or "the person mentioned in the comments." Those rules look clever until they create unwanted work, miss local knowledge, or expose information the agent should not use.

If you need triage suggestions, split suggestion from assignment. The agent can write a private recommendation or add a label such as `needs-owner`. A human then assigns the issue. This preserves speed without asking a model to make a social decision from partial context.

A project-level permission is often too coarse for this job. Many trackers give an account permission to assign any member who is assignable in a project, while the team wants a narrower rule: only preserve the existing owner, or only assign to a rotation. Enforce that narrower rule in the action gateway, with an allowlist or an authoritative lookup. Do not depend on the agent to remember it.

When an assignment does proceed, log the previous assignee and the new assignee. The visible tracker change may show only the current owner after later edits. The action record should preserve who changed it and under which run.

## Approval must expose the exact mutation

One approval per agent session is useful for establishing that a known process may act. It does not settle whether every action in that run deserves the same level of trust. A session that can read ten issues may safely do so, while its request to move an issue to Done should still stop for review.

Build approvals around consequence. A routine, reversible comment on an internal issue may proceed after the session receives approval. An assignment, a terminal transition, a change to priority, or a comment that reaches an external collaborator should require a separate decision. The boundary should also account for volume. Fifty permissible comments in a minute can still damage a project's signal.

The approval card needs enough detail to let a human refuse intelligently:

- Agent process identity and the run that requested the action.
- Tracker, project, and issue identifier.
- Existing and proposed status or assignment.
- Full comment text, or the exact changed field values.
- Expected side effect, such as a notification or workflow rule, when known.

Avoid approval text like "Allow issue tracker write access?" That asks a human to approve a category while hiding the individual action. People approve broad prompts to get work moving, then the prompt becomes background noise.

Sallyport uses a vault gate, per-session authorization, and optional per-call approval for credentials it holds. That model fits tracker automation when you reserve the per-call choice for sensitive mutations, but the gateway still needs narrow action definitions. Approval cannot repair an overly broad `update_issue` call after the fact.

Approval fatigue is a design failure, not evidence that humans dislike control. If every harmless read or predictable transition asks for a click, people will approve without reading. Reduce the number of prompts by reducing the agent's action surface and separating ordinary operations from consequential ones.

## The audit trail must answer who, what, and why

Tracker history is useful but insufficient. It can show that an integration account changed an issue, yet it often cannot answer which local process initiated the call, what the agent had been asked to do, whether a human approved it, or what response the tracker returned. You need an action record outside the tracker.

Record one immutable event per attempted outbound call. Include the request before transmission, the outcome, and an identity that reaches farther than the service account name. A practical record shape is:

```json
{
  "event_id": "evt_01JQ...",
  "time": "2025-03-08T14:22:11Z",
  "agent_process": "signed-authority and process instance",
  "session_id": "sess_7d91",
  "approval": "per-call approved",
  "operation": "transition_issue",
  "target": {"tracker": "engineering", "issue": "ENG-1842"},
  "before": {"status": "In Progress"},
  "request": {"transition": "Blocked", "reason": "dependency conflict"},
  "response": {"status": 200, "tracker_change_id": "..."}
}
```

Redact credentials and any secret-bearing headers before storing the request. Also consider comment content carefully. The comment must be recorded if you want later accountability, but access to the audit store should match the sensitivity of the project.

Use append-only storage or a hash chain so an operator cannot quietly edit an embarrassing event after an incident. Sallyport projects its session and call journals from an encrypted, hash-chained audit log, and `sp audit verify` can verify the chain offline over ciphertext. That is a useful property when you need to check a record without first trusting a running service.

Logging after a successful request is not enough. Log denied requests, failed calls, and approval refusals too. A series of rejected assignment attempts may reveal an agent loop or a malicious instruction in issue content before any visible tracker damage occurs.

## A failed update should stop instead of guessing

The dangerous tracker integration is the one that "helps" after it gets an error. It retries against a similar issue, changes a status by direct field edit after a workflow transition fails, strips a validation error from a comment, or selects the first matching user. Those fallbacks turn a contained failure into a wrong action.

Consider a realistic failure. An agent receives a task to move the issue that tracks a dependency update into review. It searches for "dependency update," gets several results, and selects an older issue with a similar title. Its broad update credential lets it set the status and add a comment. The agent then notices that the expected reviewer label is absent and assigns the person who authored a related change. Every individual API call succeeds. The result is still wrong in three ways: wrong issue, false workflow state, and unsolicited assignment.

A safer implementation makes the request fail at several points. The caller must supply an exact issue ID from an earlier approved context. The transition service checks that the issue has the expected source status and repository reference. The agent cannot assign anyone through the transition operation. If it wants to add a comment, the system asks for separate approval when the project is external or the text carries a status claim.

Set these failure rules explicitly:

1. Reject ambiguous issue references. A title search can propose candidates, but cannot authorize a write.
2. Reject stale state. If the issue changed after the agent read it, fetch it again and require a new decision.
3. Reject unavailable transitions. Never substitute a direct field edit merely because it works.
4. Reject unmapped users. Never choose a person from fuzzy name matching.
5. Stop retries after a semantic error. Retrying a network timeout is reasonable; retrying "transition not allowed" is not.

Idempotency matters too. Network retries can post duplicate comments or perform a transition twice if the client loses the response. Generate an action ID before the call and store it. If the tracker supports an idempotency mechanism, send that ID through its supported channel. If it does not, check the audit record and issue history before retrying a write.

## Credentials should authorize action paths, not raw access

Keeping a tracker token out of model context is necessary. It prevents an agent from printing the token, sending it to another tool, or using it from an unapproved machine. It does not limit what the action service can do with that token.

Put the credential in a gateway that owns outbound tracker calls. The agent asks for a named action. The gateway validates the target, field set, state contract, approval requirement, and rate or volume limits before it injects credentials and sends the request. The agent receives the result, not the credential.

Where the tracker supports separate scoped tokens, use them. A read worker should not share a write credential. A comment worker should not hold administration or project-configuration permission. Where the tracker only offers broad project write permission, the gateway becomes more important because it enforces the smaller contract the vendor permission model cannot express.

Do not put a bearer token in an environment variable available to an agent shell and call that containment. The token may never enter the model's text context, but shell tools, child processes, debug output, and configuration files can still expose it. Keep the secret in the credential-owning application and make the agent interact through a local protocol that carries an action request rather than a secret.

This architecture also makes revocation meaningful. Stop the agent process, revoke its session, or disable its action identity, and the gateway blocks future calls immediately. If every agent copied the raw token, revocation means rotating the token and chasing unknown copies.

## Build the first integration around one boring action

Start with a single transition that has an unambiguous meaning, such as moving a specifically identified issue from In Progress to Blocked when a build system reports a named dependency failure. Do not begin with full issue editing because the vendor made it easy.

Implement the action contract, project allowlist, source-state check, explicit comment template, and audit event. Then test the unpleasant cases: a wrong project ID, a closed issue, two matching issue titles, a stale status, a denied approval, an interrupted response, and a comment containing a pasted secret. If the system cannot explain exactly what it will do in each case, it is not ready to run unattended.

The work that follows is less glamorous than a generic agent tool, but it stays understandable. Add one action class at a time, and make each new authority earn its place with a clear rule, a visible approval path where needed, and records that will still make sense after a stressful incident.
