Command-line access to Confluence and Jira. Exports and updates wiki pages as Markdown, and reads and writes Jira issues, sprints, links and worklogs.
Works with both Atlassian Cloud (*.atlassian.net) and Server / Data
Center.
Requires uv.
git clone <repo-url> && cd atlassian-local-cli
make setup
uv tool install . --reinstallInstalls atlassian-local-cli into ~/.local/bin/.
Download your platform's binary from the Releases page:
| Asset | Platform |
|---|---|
atlassian-local-cli-macos-arm64 |
macOS, Apple Silicon |
atlassian-local-cli-macos-x86_64 |
macOS, Intel |
atlassian-local-cli-linux-x86_64 |
Linux, x86_64 |
chmod +x atlassian-local-cli-*
mv atlassian-local-cli-* /usr/local/bin/atlassian-local-cliCredentials live in ~/.config/atlassian-local-cli/.env. Create it with
context add, which prompts for each value and reads tokens without echoing
them:
atlassian-local-cli context add defaultOr write the file by hand, copying .env.example as a starting point:
WIKI_URL=https://wiki.example.com/
WIKI_USERNAME=your-username
WIKI_TOKEN=your-confluence-token
JIRA_URL=https://jira.example.com/
JIRA_TOKEN=your-jira-token
# JIRA_USERNAME=you@example.com # Cloud only — see below
Verify it with atlassian-local-cli context show (tokens are masked).
| Setting | Effect |
|---|---|
WIKI_URL under *.atlassian.net |
Confluence uses basic auth — requires WIKI_USERNAME |
WIKI_USERNAME set |
Confluence uses basic auth (username + token) |
| Neither | Confluence uses a Bearer token |
JIRA_URL under *.atlassian.net |
Jira uses basic auth — requires JIRA_USERNAME |
Any other JIRA_URL |
Jira uses a Bearer Personal Access Token |
WIKI_AUTH / JIRA_AUTH = basic/bearer |
Overrides the URL rules above |
Cloud and Server reject each other's scheme, so a Cloud URL selects basic auth on its own. If the matching username is missing, the tool says which key to set instead of failing with a bare HTTP error.
- Cloud: set
WIKI_USERNAME/JIRA_USERNAMEto your account email and the token to an API token. One API token works for both Jira and Confluence on the same site. - Server / Data Center: set the token to a Personal Access Token and leave
JIRA_USERNAMEunset.WIKI_USERNAMEstill selects basic auth here, which is how Confluence auth has always been chosen. WIKI_AUTH/JIRA_AUTHare needed only for Cloud on a custom domain, which the*.atlassian.netcheck cannot recognise, or to opt anatlassian.nethost out of basic auth.
Contexts work like kubectl's. The .env above is the default context; other
accounts live in ~/.config/atlassian-local-cli/contexts/<name>.env.
atlassian-local-cli context add work # create one, prompting for each value
atlassian-local-cli context list # list contexts; active is marked *
atlassian-local-cli context current # print the active context name
atlassian-local-cli context show work # show resolved config, tokens masked
atlassian-local-cli context use work # make it the persistent default
atlassian-local-cli context unset # revert the default to 'default'Use a context for a single command by placing --context before the
subcommand:
atlassian-local-cli --context work jira-meContexts resolve in this order:
- the
--contextflag - the context set by
context use default
Shell environment variables override values from these files, so an exported
JIRA_TOKEN in your shell profile defeats every context. context add warns
when it detects this.
context add takes a flag for each value, which is useful for scripting.
Anything not passed is prompted for, and prompting is skipped when stdin is not
a terminal. Tokens passed as flags are visible in your shell history and in
ps:
atlassian-local-cli context add work \
--jira-url https://acme.atlassian.net \
--jira-username me@acme.com \
--jira-token "$TOKEN"It writes 0600 files and refuses to overwrite an existing context without
--force. It does not change the active context.
| Flag | Description |
|---|---|
--version, -v |
Print the installed version |
--context <name> |
Use a named context for this command; must precede the subcommand |
# Export a page to markdown
atlassian-local-cli wiki-export 12345
atlassian-local-cli wiki-export 12345 -o page.md
atlassian-local-cli wiki-export 12345 -o page.md --attachments # images round-trip
# Update a page from a markdown file
atlassian-local-cli wiki-update 12345 page.md
# List a page's attachments, or download them
atlassian-local-cli wiki-attachments 12345
atlassian-local-cli wiki-attachments 12345 -o ./attachments
atlassian-local-cli wiki-attachments 12345 -o . --match '*.sql'
atlassian-local-cli wiki-attachments 12345 --json
# Attach local files to a page
atlassian-local-cli wiki-attach 12345 report.pdf query.sql
atlassian-local-cli wiki-attach 12345 report.pdf --replace
atlassian-local-cli wiki-attach 12345 build.log --name 2026-09-16.log --comment "nightly run"
# Delete an attachment (--yes is required)
atlassian-local-cli wiki-attachment-delete 12345 report.pdf --yes
atlassian-local-cli wiki-attachment-delete 12345 --id att7 --yes
# Comments (bodies are markdown, converted to Confluence storage format)
atlassian-local-cli wiki-comments 12345
atlassian-local-cli wiki-comments 12345 --location resolved
atlassian-local-cli wiki-comments 12345 --json
atlassian-local-cli wiki-comment 12345 --body "Checked the **replica lag**"
cat notes.md | atlassian-local-cli wiki-comment 12345 --body-file -
atlassian-local-cli wiki-comment-delete 12346 --yes
# Create a new page
atlassian-local-cli wiki-create SPACE "Page Title" content.md
atlassian-local-cli wiki-create SPACE "Page Title" content.md --parent 12345
# Delete a page (moves to trash; --yes is required)
atlassian-local-cli wiki-delete 12345 --yes
atlassian-local-cli wiki-delete 12345 --yes --cascade # also delete child pages
# Dump raw HTML / list macros (debug a failing or hanging export)
atlassian-local-cli wiki-raw 12345 # storage format (default)
atlassian-local-cli wiki-raw 12345 --format export # rendered format
atlassian-local-cli wiki-raw 12345 --format both -o raw.html
atlassian-local-cli wiki-raw 12345 --macros # list top-level macrosExported files carry YAML frontmatter (page ID, space, version, author, dates,
URL) and a # Title heading. Both are stripped automatically on update and
create, so an exported file can be edited and pushed straight back.
--attachments rewrites the exported image links from server URLs to bare
filenames and downloads those files next to the markdown, so the page survives
wiki-export → edit → wiki-update with its images intact. It needs -o,
because stdout has no directory to download into. Without the flag, export is
unchanged and image links stay absolute. A file the page references but doesn't
own is named on stderr rather than skipped quietly, and wiki-update warns
before uploading an image whose file is missing from disk.
wiki-comment bodies are markdown and go through the same converter as
wiki-update, so lists, code blocks and **bold** render properly on the page.
Listing converts the rendered comment HTML back to markdown. --location
narrows to footer, inline or resolved comments; replies are indented under
their parent. wiki-comment-delete takes a comment id (from wiki-comments),
requires --yes, and refuses an id that turns out to be a page.
wiki-attachments lists every attachment on the page (name, size, media type,
version, date); adding -o <dir> downloads them into that directory, creating
it if needed. Existing files are overwritten, so re-running picks up newer
versions of an attachment. --match takes a shell glob against the filename and
applies to both listing and download. Note that wiki-export does not rewrite
attachment links in the markdown to local paths — the exported body still points
at the server.
wiki-attach uploads local files as attachments. It refuses a filename the page
already carries unless you pass --replace, because Confluence upserts on
filename: an unguarded upload supersedes whatever is there, including a file
someone else put on the page. With --replace it reports the version it bumped
(Replaced report.pdf (v2 -> v3)). Missing local files abort the whole command
before the first upload, since the uploads themselves are sequential and not
transactional. --name attaches one file under a different name, and the
collision check runs against that name, not the local one. Attachments are
typed from that name, so a .csv or .sql is stored as text/csv /
application/x-sql rather than the application/binary the underlying
library falls back to for anything outside its 8 known extensions.
wiki-attachment-delete takes a filename — unique per page, which is what
wiki-attach upserts on — or an --id from wiki-attachments --json. It
requires --yes and prints the attachment it removed.
# Print current user
atlassian-local-cli jira-me
# View an issue
atlassian-local-cli jira-get PROJ-123
# Open an issue in your browser
atlassian-local-cli jira-open PROJ-123
atlassian-local-cli jira-open PROJ-123 --print-url # print URL only
# List your assigned tasks
atlassian-local-cli jira-my-tasks
atlassian-local-cli jira-my-tasks --status closed
atlassian-local-cli jira-my-tasks --project PROJ --status open
atlassian-local-cli jira-my-tasks --status-name "Reviewing"
atlassian-local-cli jira-my-tasks --json --limit 10
# Search — raw JQL, builder flags, or both
atlassian-local-cli jira-search --jql 'project = PROJ AND text ~ "login"'
atlassian-local-cli jira-search --assignee me --status open --type Bug
atlassian-local-cli jira-search --project PROJ --order-by priority --reverse
atlassian-local-cli jira-search --project PROJ --csv > issues.csv# Create an issue
atlassian-local-cli jira-create --project PROJ --summary "Fix login" --type Bug --priority High
atlassian-local-cli jira-create --project PROJ --summary "New epic" --type Epic
atlassian-local-cli jira-create --project PROJ --summary "Task" --epic PROJ-100 # under an epic
# Update individual attributes (any combination)
atlassian-local-cli jira-update PROJ-123 --summary "New title"
atlassian-local-cli jira-update PROJ-123 --priority High --assignee jdoe
atlassian-local-cli jira-update PROJ-123 --assignee none # unassign
atlassian-local-cli jira-update PROJ-123 --epic PROJ-100 # link to epic
atlassian-local-cli jira-update PROJ-123 --epic none # unlink epic
atlassian-local-cli jira-update PROJ-123 --label backend --label urgent # replace
atlassian-local-cli jira-update PROJ-123 --add-label hotfix --remove-label stale # mutate
atlassian-local-cli jira-update PROJ-123 --description-file notes.md
atlassian-local-cli jira-update PROJ-123 --field 'customfield_10010={"value":"X"}'
# Transition an issue
atlassian-local-cli jira-transition PROJ-123 # list available transitions
atlassian-local-cli jira-transition PROJ-123 "In Progress" # by status name or transition ID
atlassian-local-cli jira-transition PROJ-123 Done --resolution "Won't Do"
# Clone, delete
atlassian-local-cli jira-clone PROJ-123 --replace "Q1:Q2"
atlassian-local-cli jira-delete PROJ-123 --yes # --yes is required
atlassian-local-cli jira-delete PROJ-123 --yes --cascade # also delete sub-tasks--resolution only works on transitions whose screen includes the resolution
field.
# Add and list comments
atlassian-local-cli jira-comment PROJ-123 --body "Looks good to me"
cat notes.md | atlassian-local-cli jira-comment PROJ-123 --body-file -
atlassian-local-cli jira-comments PROJ-123
atlassian-local-cli jira-comments PROJ-123 --json
# Edit and delete comments (IDs come from jira-comments)
atlassian-local-cli jira-comment-update PROJ-123 10042 --body "Corrected"
atlassian-local-cli jira-comment-update PROJ-123 10042 --body "Typo fix" --no-notify
atlassian-local-cli jira-comment-delete PROJ-123 10042 --yes
# Log work (Jira time format: 1w=5d, 1d=8h)
atlassian-local-cli jira-worklog PROJ-123 --time "2h 30m" --comment "Pairing"
atlassian-local-cli jira-worklog PROJ-123 --time "1d"jira-comment-update replaces the whole body and prints the body it replaced —
Jira shows only the new text after an edit, so that output is the only remaining
copy of the old one. --no-notify skips the watcher email. jira-comment-delete
prints the comment before removing it and requires --yes.
atlassian-local-cli jira-link-types # list link types
atlassian-local-cli jira-link PROJ-1 PROJ-2 --type Blocks # PROJ-1 blocks PROJ-2
atlassian-local-cli jira-link PROJ-1 PROJ-2 --type Relates --comment "see this"
atlassian-local-cli jira-unlink 10042 # remove link by ID
atlassian-local-cli jira-link-epic PROJ-200 PROJ-201 --epic PROJ-100 # bulk-link to epic
atlassian-local-cli jira-epics --project PROJ # list epics
atlassian-local-cli jira-epic-issues PROJ-100 # list children of epicatlassian-local-cli jira-sprints --board 42 # list sprints on board
atlassian-local-cli jira-sprints --board 42 --state active # active, closed, future
atlassian-local-cli jira-sprint-add 5 PROJ-1 PROJ-2 PROJ-3 # add issues to sprint
atlassian-local-cli jira-sprint-issues 5 # list issues in sprint| Flag | Description | Example |
|---|---|---|
--status |
Status category: open, closed, all |
--status closed |
--status-name |
Exact status name | --status-name "Reviewing" |
--type |
Issue type | --type Epic |
--project |
Project key | --project PROJ |
--limit |
Max results (default: 50) | --limit 10 |
--json |
JSON output, for integrations | --json |
jira-search takes a raw --jql clause, the builder filters below, or both.
When both are given they are AND-ed together.
| Flag | Description |
|---|---|
--jql |
Raw JQL clause |
--assignee |
User key, or me / none (unassigned) |
--reporter |
User key, or me |
--status |
Status category: open, closed, all |
--status-name |
Exact status name ("In Progress") |
--type / --priority / --project |
Exact match |
--label |
Repeatable; labels are AND-ed |
--order-by |
JQL field to sort by (default: updated) |
--reverse |
Sort ascending instead of descending |
--limit |
Max results (default: 50) |
--json / --csv |
Machine-readable output |
Pass any combination; only the named fields are changed.
| Flag | Notes |
|---|---|
--summary |
Replace the summary |
--description / --description-file |
Replace description; - reads stdin |
--priority |
Highest, High, Medium, Low, Lowest |
--assignee |
Username, or none to unassign |
--type |
Change issue type (e.g. Bug) |
--epic |
Epic key to link to, or none to unlink |
--label |
Replace the label set (repeatable) |
--add-label / --remove-label |
Mutate existing labels (repeatable; cannot mix with --label) |
--field key=value |
Raw field assignment; values are parsed as JSON when possible |
jira-worklog --time accepts Jira's work-week syntax. A bare integer is minutes.
| Token | Seconds | Meaning |
|---|---|---|
m |
60 | minute |
h |
3600 | hour |
d |
28800 | day = 8h |
w |
144000 | week = 5d |
Combine tokens freely: "1w 2d 3h 30m" or "2h30m".
These forms convert to native Confluence macros on upload, and back to markdown on export.
| Markdown | Confluence |
|---|---|
**bold**, *italic* |
<strong>, <em> |
~~struck~~ |
<s> (strikethrough) |
<u>text</u> |
<u> (underline — markdown has no syntax for it) |
x<sup>2</sup>, H<sub>2</sub>O |
<sup>, <sub> |
{status:DONE|green} |
Status badge |
@jdoe |
User mention |
{date:2026-03-26} |
Date |
{jira:PROJ-123} |
Jira issue link |
[TOC] |
Table of contents |
<iframe>…</iframe> |
HTML macro (iframe preserved) |
> {panel:info|Title} + > body lines |
Info / note / warning / tip / panel macro |
<details><summary>Title</summary> … </details> |
Expand macro |
| Fenced code blocks | Code macro |
|| TEXT || table row |
Full-width section header (colspan) |
<!-- page-properties --> above a table |
Page Properties macro |
<!-- page-properties-report key=value --> |
Page Properties Report macro |
Underline, superscript and subscript have no markdown syntax, so they travel as
inline HTML — which markdown renderers display natively, so the exported file
still previews correctly. Strikethrough uses GFM ~~; tildes inside inline code
or a fenced block are left alone. On export, Confluence's styled-span forms
(<span style="text-decoration: line-through">) are normalised to <s>/<u>.
Status badge colours: green, red, blue, yellow, grey.
Panel types: info, note, warning, tip, panel.
| Task | Status | Owner |
|--------------|-------------------------|--------|
| Deploy DB | {status:DONE|green} | @jdoe |
| Configure LB | {status:PENDING|yellow} | @alice |
> {panel:warning|Heads up}
> This runs against production.Every command is also a make target, for development. Each accepts
CONTEXT=<name> to override the active context for that invocation.
make setup # Install dependencies
make test # Run tests
make test-cov # Run tests with coverage
make build # Build standalone binary
make clean # Remove build artifacts
make wiki-export PAGE=12345 OUTPUT=page.md ATTACHMENTS=1
make wiki-update PAGE=12345 INPUT=page.md
make wiki-attachments PAGE=12345 OUTPUT=./attachments MATCH='*.sql'
make wiki-attach PAGE=12345 FILES="report.pdf query.sql" REPLACE=1
make wiki-attachment-delete PAGE=12345 NAME=report.pdf YES=1
make wiki-comments PAGE=12345 LOCATION=footer
make wiki-comment PAGE=12345 BODY="Looks right to me"
make wiki-comment-delete COMMENT=12346 YES=1
make wiki-create SPACE=DEV TITLE="My Page" INPUT=page.md
make wiki-delete PAGE=12345 YES=1
make wiki-raw PAGE=12345 FORMAT=storage MACROS=1
make jira-me
make jira-get ISSUE=PROJ-123
make jira-open ISSUE=PROJ-123
make jira-my-tasks JSON=1 LIMIT=10
make jira-search PROJECT=PROJ ASSIGNEE=me CSV=1
make jira-create PROJECT=PROJ SUMMARY="Fix login" TYPE=Bug
make jira-update ISSUE=PROJ-123 PRIORITY=High ASSIGNEE=jdoe
make jira-transition ISSUE=PROJ-123 STATUS="In Progress" RESOLUTION="Won't Do"
make jira-clone ISSUE=PROJ-123 REPLACE="Q1:Q2"
make jira-delete ISSUE=PROJ-123 YES=1
make jira-comment ISSUE=PROJ-123 BODY="LGTM"
make jira-comments ISSUE=PROJ-123
make jira-comment-update ISSUE=PROJ-123 COMMENT=10042 BODY="Corrected"
make jira-comment-delete ISSUE=PROJ-123 COMMENT=10042 YES=1
make jira-worklog ISSUE=PROJ-123 TIME="2h 30m"
make jira-link FROM=PROJ-1 TO=PROJ-2 TYPE=Blocks
make jira-unlink LINK_ID=10042
make jira-link-types
make jira-link-epic ISSUES="PROJ-1 PROJ-2" EPIC=PROJ-100
make jira-epics PROJECT=PROJ
make jira-epic-issues EPIC=PROJ-100
make jira-sprints BOARD=42 STATE=active
make jira-sprint-add SPRINT=5 ISSUES="PROJ-1 PROJ-2"
make jira-sprint-issues SPRINT=5
make context-add NAME=work
make context-list
make context-current
make context-show NAME=work
make context-use NAME=work
make context-unsetmake test # run the test suite
make test-cov # with coverage, terminal + HTML
make build # standalone binary at dist/atlassian-local-cliRun a single test:
uv run pytest tests/test_converters.py::TestMdToConfluenceHtml::test_status_badgeTagging v* triggers GitHub Actions, which builds the macOS (arm64, x86_64) and
Linux (x86_64) binaries and publishes a release with them attached.
# 1. Bump `version` in pyproject.toml and add a CHANGELOG.md entry
# 2. Commit both with the change they describe:
git commit -m "feat: add the thing (v2.9.0)"
# 3. Tag and push:
git tag -s v2.9.0 -m "Release v2.9.0"
git push origin main --follow-tags