Install
$ agentstack add skill-eugenelim-agent-ready-repo-confluence-publisher ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Confluence Publisher
Publish a single page to Confluence — create or update — from Markdown, storage XHTML, or plain text. Companion to confluence-crawler: same credentials namespace, same flavor support, opposite direction.
Instructions
You are a Confluence publishing agent. Authentication, REST mechanics, optimistic-locking retries, and the Markdown→storage conversion live in scripts/. Do not re-implement any of that; invoke the script with the right flags and report the result.
Flavor support
Same as the crawler:
- Atlassian Cloud (
*.atlassian.net) — Basic auth with email + API
token. Base URL must include /wiki.
- Confluence Server / Data Center — Bearer auth with a Personal
Access Token.
Flavor is auto-detected from the base URL; override via CONFLUENCE_FLAVOR=cloud|server if needed.
Configuration location
Credentials are resolved by the build-projected credentials_shim.load_credentials through Tier 1 (env) → Tier 2 (OS keyring) → Tier 3 dotfile. The dotfile lives at ~/.agentbundle/credentials.env. The declared schema is at references/creds-schema.toml and shares the confluence namespace with confluence-crawler — if either skill has been configured, this one works.
| Key | Required | Notes | |---|---|---| | CONFLUENCE_BASE_URL | yes | Cloud: https://.atlassian.net/wiki. Server: https://confluence.corp.example.com. | | CONFLUENCE_API_TOKEN | yes | Cloud API token or Server PAT. | | CONFLUENCE_EMAIL | Cloud only | Atlassian account email. | | CONFLUENCE_FLAVOR | no | cloud or server. Auto-detected from URL host. |
Populate any tier by running credential-setup skill.
Security rules (non-negotiable)
- Secrets live only in
~/.agentbundle/credentials.env
(mode 0600 on POSIX; DACL-restricted on Windows), the OS keyring, or process environment variables. Never read that file, print it, or echo the token.
- Never put the token on the command line. The primitive
refuses flags like --token / --api-token / --bearer / --pat / --password and exits — do not work around it.
- If
--checkreports missing or invalid creds, tell the user to run
credential-setup skill themselves. It's interactive — do not run it for them.
Step 1: Verify the environment
python -m pip install -r requirements.txt
python scripts/publish_page.py --check
- Exit code 0 → authenticated, proceed.
- Exit code 2 → the user must act (credentials missing/invalid/expired). Tell
the user to run credential-setup skill themselves (interactive — they run it, not you). Stop here.
- Any other non-zero → see When a request fails.
When a request fails
The CLI uses a banded exit-code contract; read the stderr message for the specific cause, then act on the band:
| Exit | Band | What to do | |---|---|---| | 0 | success | proceed | | 1 | functional error — server 5xx, transport, keychain hard-fail, unexpected | surface the message to the user; don't loop or retry blindly | | 2 | user must act — credentials (401/403), a publish conflict, or a target/input the user must fix | follow the NEED-INPUT: message: re-auth via credential-setup, resolve the conflict, or fix the target — then retry |
Tier2HardFailError (OS keyring unavailable) or an unprojected shim surface as exit 1 with a message naming the cause.
Step 2: Decide how to identify the target page
In order of robustness — use whichever the user gave:
- By page ID or URL (preferred).
--page-id 12345 or --url https://acme.atlassian.net/wiki/spaces/ENG/pages/12345/Some+Title. The page ID is parsed out of the URL. Idempotent.
- By frontmatter — if the input file was produced by
confluence-crawler it carries confluence_id (and optionally version, space_key) in YAML frontmatter. --from-frontmatter reads it. This is the round-trip case (crawl → edit → publish back).
- By space + title —
--space ENG --title "My Page" [--parent-id 999].
Looks up by title; if found, updates; if not, creates. Title lookups are fragile (titles change); prefer modes 1 and 2 when an ID is available.
If none of these are supplied, the script exits 2 and asks which. Do not guess.
Step 3: Publish
Pick the form that matches the user's request:
# Update an existing page by ID, from Markdown:
python scripts/publish_page.py --page-id 12345 --input report.md
# Same, but from a Confluence URL:
python scripts/publish_page.py --url 'https://acme.atlassian.net/wiki/spaces/ENG/pages/12345/Foo' --input report.md
# Round-trip case — the markdown came from confluence-crawler:
python scripts/publish_page.py --from-frontmatter --input crawled/eng-handbook.md
# Lookup-then-upsert by title:
python scripts/publish_page.py --space ENG --title "Q2 Report" --parent-id 999 --input report.md
# Plain text body (one paragraph per line):
python scripts/publish_page.py --page-id 12345 --input - --input-format text # stdin
# Already-rendered storage XHTML:
python scripts/publish_page.py --page-id 12345 --input snippet.xhtml --input-format storage
# Dry-run — print what would be sent, do not call write APIs:
python scripts/publish_page.py --page-id 12345 --input report.md --dry-run
Flags:
| Flag | Meaning | |---|---| | --check | Verify credentials and connectivity, then exit. | | --page-id ID | Update this page (preferred). | | --url URL | Parse page ID from a Confluence URL. | | --from-frontmatter | Read confluence_id (and optional version) from input file's YAML frontmatter. | | --space KEY --title TITLE | Lookup-then-upsert by title. --parent-id ID optional. | | --input PATH or - | Source file (or - for stdin). Required. | | --input-format | markdown (default), storage, text. | | --version-comment TEXT | Recorded on the new page version. Defaults to a generic message. | | --attach PATH (repeatable) | Upload file as a page attachment; Markdown image refs whose target filename matches an attachment get rewritten to `. | | --label LABEL (repeatable) | Apply labels after publish. | | --dry-run | Print the rendered storage XHTML and planned operation; no writes. | | --insecure | Disable TLS verification (Server/DC w/ self-signed). User-requested only. | | --verbose` | Debug logging. |
Step 4: Interpret the output
On success the script prints:
OK: page 12345 (version 8) — https://acme.atlassian.net/wiki/spaces/ENG/pages/12345/Foo
On a 409 (someone else edited between read and write) the script re-reads the page once and retries with the new version number. If the second attempt still conflicts, it surfaces the error — tell the user a human edited concurrently and ask them to re-run.
Behavior notes
- Update vs create.
--page-id/--urlalways updates; never
creates a new page at a specific ID. --from-frontmatter updates the page named in the frontmatter. --space + --title updates if a page with that title exists in the space, otherwise creates one (under --parent-id if given, otherwise at the space root).
- Title. On update, the title is taken from
--titleif given, the
first # H1 of the markdown if not (markdown input only), and the existing page title as a final fallback. On create, --title is required (or the first H1 if --input-format markdown). Heads-up: for markdown input, the H1 overrides the existing page title even on a routine re-publish — if you don't want a rename, pass --title explicitly or strip the H1.
- Attachment ordering. On an update of an existing page,
attachments upload before the body update so `` references resolve immediately. On a create, attachments upload after the page is created (the page must exist first); the body's image refs render broken for the subsecond gap between create and the attachment uploads. Failure semantics are not symmetric: if an update's attachment uploads partly succeed and then raise, the body update is skipped — the page still shows the prior body but now has the new attachments orphaned on it; re-running is idempotent because Confluence dedupes attachment uploads by filename. On create, an attachment failure after a successful create leaves the page in place with the body referencing un-uploaded files.
- Version comment. Recorded on the new version; helps reviewers see
why an agent edited. Default: Published by confluence-publisher.
- Markdown conversion. Renders CommonMark via
markdown-it-py,
then post-processes to storage XHTML. The macro round-trip mirrors confluence-crawler's allowlist: info / warning / note / tip / panel / expand / code. Bold-leadin admonitions (**Note:** …, **Tip:** …, **Warning:** …, **Info:** …, **Important:** …) become the matching macro. Other Markdown is rendered as standard XHTML elements Confluence accepts.
- Attachments.
--attachuploads each file as a page attachment.
After upload, Markdown image references in the input whose target filename matches an attached filename are rewritten to ``. Files not matched are uploaded anyway (the user might link them by other means).
- Labels. Applied after the page write; failure to apply labels is
reported but does not roll back the page write.
- Mermaid / PlantUML. Out of scope. Run the
mermaid-renderer
skill first to pre-render fenced `mermaid blocks to PNGs, then pass those PNGs via --attach to this skill.
Don't
- Don't read
~/.agentbundle/credentials.envfrom skill body. - Don't print or log the token.
- Don't run
credential-setupskill non-interactively or pipe the token into it. - Don't write your own REST calls to Confluence — extend the scripts
and surface the gap to the user if a flag is missing.
- Don't auto-resolve a title collision by appending suffixes — surface
the ambiguity (the script does this) and ask which page to update.
- Don't assume
--insecureis safe to add by default; only when the
user explicitly accepts it.
- Don't pass
--forceto bypass a 409 — there is no such flag.
Concurrent edits need human attention.
Edge cases
- Page moved between spaces between when the user got the URL and
when you publish: the page ID still resolves; the publish targets the page in its current space.
- Title collision in lookup mode: if
GET /rest/api/content?spaceKey=X&title=Y
returns more than one result (rare but possible across page states), the script exits 2 with the list of IDs. Ask the user which to target via --page-id.
- Frontmatter without
confluence_id: the script exits 2 and asks
for one of the other identification flags.
- Storage-format input with invalid XHTML: the API returns 400;
the script surfaces the error message. Don't try to fix it client-side — ask the user.
- Network failure mid-publish. Reads (the version probe) are
retried by the client. Writes are not — a failed PUT/POST means the page is in its prior state; re-run.
- Large pages. Confluence soft-caps storage at ~5 MB. Beyond that,
break the content into linked sub-pages; this skill doesn't do that for you.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: eugenelim
- Source: eugenelim/agent-ready-repo
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.