MITupdated 10d ago
Every command in this plugin begins by sourcing the shared bootstrap, which loads and validates .env and the user overlay. If either is missing or incomplete, the bootstrap aborts with a clear message and the skill must stop.
What can you do with Jira Context?
name: jira-context description: How to call Jira Cloud's API and how the plugin resolves user-specific project/workflow IDs
Plugin bootstrap (ALWAYS run first)
Every command in this plugin begins by sourcing the shared bootstrap, which
loads and validates .env and the user overlay. If either is missing or
incomplete, the bootstrap aborts with a clear message and the skill must stop.
source "${CLAUDE_PLUGIN_ROOT}/lib/bootstrap.sh"
After it returns successfully, the following variables are in scope:
| Variable | Source | Purpose |
|---|---|---|
JIRA_BASE_URL |
.env |
Base URL for every API call (no trailing slash). |
JIRA_EMAIL |
.env |
Reference only for building prompts / display; never pass on a curl CLI. |
JIRA_API_TOKEN |
.env |
Raw secret. Never reference directly on a CLI — use $JIRA_CURL_CONFIG. |
JIRA_OVERLAY_FILE |
bootstrap-exported path | Path to the user overlay YAML. |
JIRA_PYTHON |
bootstrap-detected | Python interpreter verified to have PyYAML. |
JIRA_CURL_CONFIG |
bootstrap-exported path | 600-perm file containing the Authorization header. Pass to curl via --config "$JIRA_CURL_CONFIG". |
Never hardcode a project key, transition ID, account ID, custom field ID, or link type in a command. Read it from the overlay.
Reading the overlay
Pipe the overlay file into Python via stdin — no path interpolation, no quote-escaping, no OS-specific path conversion. Works identically on Linux, macOS, and Git Bash on Windows:
support_key=$("$JIRA_PYTHON" -c '
import sys, yaml
d = yaml.safe_load(sys.stdin)
role = d.get("triage", {}).get("source_project_role", "support")
print(next((p["key"] for p in d.get("projects", []) if p.get("role") == role), ""))
' < "$JIRA_OVERLAY_FILE")
Commands should always resolve project keys by role, not by literal
key. The role to look up comes from triage.source_project_role (or the
relevant *.default_project_role for task creation). A literal default
like support is only the fallback when the overlay does not declare a
role explicitly.
API reference
- Base URL:
$JIRA_BASE_URL - Auth:
curl --config "$JIRA_CURL_CONFIG" ...— the Authorization header is read from a 600-perm file, never from a CLI argument.
Endpoints this plugin uses
| Purpose | Method + path |
|---|---|
| Search issues (JQL) | POST /rest/api/3/search/jql (GET is deprecated, returns total: 0) |
| Issue detail | GET /rest/api/3/issue/{key} (use ?expand=changelog for history) |
| Create issue | POST /rest/api/3/issue |
| Update issue | PUT /rest/api/3/issue/{key} |
| List transitions | GET /rest/api/3/issue/{key}/transitions |
| Apply transition | POST /rest/api/3/issue/{key}/transitions with {"transition":{"id":"N"}} |
| Add comment | POST /rest/api/3/issue/{key}/comment (body in ADF) |
| Create issue link | POST /rest/api/3/issueLink |
| Current user | GET /rest/api/3/myself |
| List boards | GET /rest/agile/1.0/board?projectKeyOrId={KEY} |
| List sprints | GET /rest/agile/1.0/board/{boardId}/sprint?state=active,future |
| Add issues to sprint | POST /rest/agile/1.0/sprint/{sprintId}/issue with {"issues":["KEY-1"]} (204, max 50 keys) |
| Upload attachment | POST /rest/api/3/issue/{key}/attachments (multipart, header X-Atlassian-Token: no-check) |
| Resolve a media id | GET /rest/api/3/attachment/content/{id} — answers 303; the UUID is in the Location path |
Sprints are not set at creation
The Sprint custom field shows up on the create screen, but it is read-only
there: passing it in POST /rest/api/3/issue is silently ignored and the issue
lands in the backlog. Putting an issue in a sprint is always a second call, to
the Agile API above. Which projects need it, and which board answers for each,
come from the overlay's sprints block — never hardcode a board or sprint ID,
and never create a sprint on the user's behalf.
A project with more than one scrum board is common (a live board next to an
(OLD) one). When discovery returns several, ask instead of guessing: the
wrong board means a sprint nobody looks at, which is the same as the backlog.
Issue keys in a body are links, never bare text
Every issue key that shows up in a description or a comment — the tasks a triage
comment spun off, a related ticket, a release — is written as a clickable link
to $JIRA_BASE_URL/browse/<KEY>. A key rendered as plain or bold text costs the
reader a copy, a switch to the search box and a paste before they can see what
the comment is talking about.
The link mark composes with strong, so a key stays emphasised and becomes
clickable:
{"type":"text","text":"PROJ-123",
"marks":[{"type":"link","attrs":{"href":"https://your.atlassian.net/browse/PROJ-123"}},
{"type":"strong"}]}
Build the href from $JIRA_BASE_URL — never from a hardcoded host.
For a key that stands on its own line, inlineCard is the richer form: Jira
resolves it into a smart card carrying summary, status and assignee.
{"type":"inlineCard","attrs":{"url":"https://your.atlassian.net/browse/PROJ-123"}}
inlineCard is a standalone inline node and accepts no marks, so it cannot be
bolded and cannot sit inside a sentence you also want emphasised. Inside a
sentence, use the text + link form above.
Writing the link does not replace the issue link. A body that names a key must
also carry the real relationship via POST /rest/api/3/issueLink, or the panel
of linked items stays empty and only someone who reads the whole comment finds
out the ticket produced anything.
Attachments and inline images
Neither a file nor a picture in the body can ride along in
POST /rest/api/3/issue. An illustrated description is always three calls:
POST /rest/api/3/issue— create the issue, description without media.POST /rest/api/3/issue/{key}/attachments— multipart upload, one call per file, with the headerX-Atlassian-Token: no-check.PUT /rest/api/3/issue/{key}— rewrite the description, this time with a media node pointing at what you just uploaded.
Step 3 is where it bites. The ADF media node addresses the file by its Media
Services UUID, not by the numeric id the upload returns; hand it the
numeric id and Jira answers 400 ATTACHMENT_VALIDATION_ERROR with an empty
errors object and no further hint. No attachment endpoint carries that UUID
in its JSON. It surfaces in exactly one place — the Location of the 303 that
GET /rest/api/3/attachment/content/{id} answers with:
https://api.media.atlassian.com/file/<uuid>/binary?token=...
lib/jira-media.sh wraps all of that. Source it after the bootstrap:
source "${CLAUDE_PLUGIN_ROOT}/lib/bootstrap.sh"
source "${CLAUDE_PLUGIN_ROOT}/lib/jira-media.sh"
# one line per file: attachmentId <TAB> mediaId <TAB> filename
jira_attach PROJ-123 ./shot.png ./report.pdf
| Function | What it does |
|---|---|
jira_attach ISSUE_KEY FILE... |
Uploads and resolves each file, printing attachmentId, mediaId and the basename as TSV. |
jira_media_id ATTACHMENT_ID |
Numeric attachment id → Media Services UUID. |
jira_adf_image MEDIA_ID [ALT] [WIDTH] |
Prints the mediaSingle node for an image. |
jira_adf_file MEDIA_ID [ALT] |
Prints the mediaGroup node — the file card Jira shows for anything it cannot render inline. |
Building the nodes by hand is fine too; the shapes are:
{"type":"mediaSingle","attrs":{"layout":"center","width":520,"widthType":"pixel"},
"content":[{"type":"media","attrs":{"type":"file","id":"<uuid>","collection":"","alt":"shot.png"}}]}
collection has to be there and has to be the empty string. The file's own
pixel dimensions are optional; display size lives on the wrapper, as
mediaSingle.attrs.width plus widthType: "pixel". Omit it and Jira draws the
image at roughly 250px, which is too small to read a screenshot. The same node
works unchanged in a comment body — a picture in a comment costs the upload
plus the ordinary POST /issue/{key}/comment.
Two dead ends, both already tested — don't spend the round trip:
mediawithtype: "external"and aurlis accepted by the API and survives the round trip in the stored ADF, but the issue view renders it as Preview unavailable. There is no one-call illustrated create.PUT /rest/api/2/issue/{key}with wiki markup (!file.png!) does work — Jira resolves the filename and writes the UUID itself. But it replaces the entire description with the converted markup, so it only makes sense when you were overwriting the description anyway.
The token in that Location header is a live short-lived read credential for
the file. Parse the UUID out of the path and discard the rest: never print,
log, or persist the header itself.
Every request must include the auth header from $JIRA_CURL_CONFIG:
curl --config "$JIRA_CURL_CONFIG" -X POST \
-H "Content-Type: application/json" \
"$JIRA_BASE_URL/rest/api/3/search/jql" \
--data "$jql_body"
Conventions the plugin follows
- JQL uses
assignee = currentUser()whenever possible — avoid hardcoding an account ID unless you need it for assignment. - Comments must be ADF, not plain text. Wrap as:
{"body":{"type":"doc","version":1,"content":[{"type":"paragraph","content":[{"type":"text","text":"..."}]}]}} - Transitions are identified by semantic names in the overlay
(
start_progress,staging,resolve,block,mr_created,stop_progress) and resolved to numeric IDs viatransitions.<PROJECT_KEY>.<name>. - When a ticket's project isn't in
transitions, fetch IDs live from/rest/api/3/issue/{key}/transitionsand suggest the user add them to the overlay.
Secret handling
JIRA_API_TOKEN is a secret. Treat it like a password:
-
Never pass it on any command line. Not via
-u "$JIRA_EMAIL:$JIRA_API_TOKEN", not via-H, not as a positional arg. On POSIX systems every command-line argument is visible to any local user viaps aux. -
Always use
--config "$JIRA_CURL_CONFIG". The bootstrap builds the Basic-auth header, base64-encodes it, and writes it into a 600-perm file; curl reads it from disk, which is not visible in process listings. -
Never echo, print, or log the token value. Not even in dry-run output.
-
Never write it to a snapshot, log file, or report.
-
Clean up the curl config when the skill is done. Add to the end of each command's flow:
rm -f "$JIRA_CURL_CONFIG"or register it as an EXIT trap at the top of a long-running skill.
What the plugin never does
- Never writes to a project that isn't declared in
projects[]. - Never closes a ticket directly to Done unless the user explicitly asks.
The default "I'm done with this" transition is whatever the overlay's
triage.target_transitionnames (typically a QA/staging step). - Never infers missing overlay values from guesses — a missing key is always an abort with a specific error.
Install
Add Jira Context to your client. Pick the one you use.
npx skills add fkdarven/jira-for-claude-codeInstalls every skill in the repository, then prompts for which to keep.
/plugin marketplace add fkdarven/jira-for-claude-codeAdds the repository as a plugin marketplace; install individual plugins with `/plugin install`.
git clone https://github.com/fkdarven/jira-for-claude-code
cp -r plugins/jira/skills/jira-context ~/.claude/skills/A skill is a plain directory. Copy it into `.claude/skills/` in a project or in your home directory.
Score
79 / 100
Good