libreoffice-writer — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited libreoffice-writer (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Use the bundled writer modules for UNO-backed Writer document work. All paths must be absolute. Bundled modules live under scripts/ in this skill directory, so set PYTHONPATH=<skill_base_dir>/scripts. If setup or runtime issues appear, check references/troubleshooting.md.
# Non-session utilities
create_document(path, source=None) # source: path to .md file to import
export_document(path, output_path, export_format) # formats: "pdf", "docx", "md"
snapshot_page(doc_path, output_path, page=1, dpi=150)
# Session (primary editing API)
WriterSession(path) -> context manager
WriterSession methods:
read_text(target: WriterTarget | None = None) -> str
insert_text(text, target: WriterTarget | None = None)
replace_text(target: WriterTarget, new_text)
delete_text(target: WriterTarget)
format_text(target: WriterTarget, formatting: TextFormatting)
insert_table(rows, cols, data=None, name=None, target: WriterTarget | None = None)
update_table(target: WriterTarget, data)
delete_table(target: WriterTarget)
insert_image(image_path, width=None, height=None, name=None, target: WriterTarget | None = None)
update_image(target: WriterTarget, image_path=None, width=None, height=None)
delete_image(target: WriterTarget)
insert_list(items: list[ListItem], ordered: bool, target: WriterTarget | None = None)
replace_list(target: WriterTarget, items: list[ListItem], ordered: bool | None = None)
delete_list(target: WriterTarget)
set_metadata(values: dict[str, str])
get_metadata() -> dict[str, str]
patch(patch_text, mode="atomic") -> PatchApplyResult
export(output_path, export_format)
reset()
close(save=True)
# Standalone patch utility
patch(path, patch_text, mode="atomic") -> PatchApplyResultWriterTargetfrom writer import WriterTarget
WriterTarget(
kind="text" | "insertion" | "table" | "image" | "list",
text=None,
after=None,
before=None,
occurrence=None,
name=None,
index=None,
)| Kind | Supported fields | Use |
|---|---|---|
text | text, after, before, occurrence | Read, replace, delete, or format matched text |
insertion | text, after, before, occurrence | Insert at a resolved boundary or after a matched span |
table | name or index | Update/delete a table |
image | name or index | Update/delete an image |
list | text, after, before, occurrence | Replace/delete one logical list block |
target to read the full document or append inserted content at the end.after and before to constrain a search window.occurrence when repeated text is expected; otherwise matching must be unique.text, after, and before anchors; single-word anchors are often too brittle for realistic prose edits.name; use index only when order is stable.from writer import TextFormatting
TextFormatting(
bold=None,
italic=None,
underline=None,
font_name=None,
font_size=None,
color=None, # named color or integer
align=None, # "left" | "center" | "right" | "justify" | "start" | "end"
line_spacing=None,
spacing_before=None,
spacing_after=None,
)Notes:
align apply to the full paragraph containing the match, not just the exact matched span.from writer import ListItem
ListItem(text="Confirm scope", level=0)level is zero-based nesting.ordered=True uses a numbering style; ordered=False uses bullets.set_metadata() accepts a dictionary of key-value pairs.title, author, subject, keywords, and description.keywords may be a comma-separated string; get_metadata() returns them as one comma-separated string.Use patch() or session.patch() to apply ordered operations.
[operation]
type = format_text
target.kind = text
target.text = Quarterly revenue grew 18%.
target.after = Financial Summary
target.before = Action Items
format.bold = true
format.align = center
[operation]
type = insert_list
target.kind = insertion
target.after = Action Items
list.ordered = false
items <<JSON
[
{"text": "Confirm scope", "level": 0},
{"text": "Review output", "level": 0},
{"text": "Update packaging", "level": 1}
]
JSONinsert_textreplace_textdelete_textformat_textinsert_tableupdate_tabledelete_tableinsert_imageupdate_imagedelete_imageinsert_listreplace_listdelete_listtarget.* fields for target definition.target.* fields; do not use a bare target.kind = insertion without anchors.format.* fields for formatting payloads.list.ordered plus JSON items for list operations.items and data must be valid JSON.<<TAG ... TAG for multiline text or JSON.atomic stops on first failure, resets the session, and persists nothing.best_effort keeps successful earlier operations and records failures.PatchApplyResult fields:
modeoverall_status = "ok" | "partial" | "failed"operations = list of PatchOperationResultdocument_persistedFor standalone patch(path, ...), document_persisted means the changes were saved to disk. For session.patch(...), it means the patch produced successful mutations in the current open session state.
from pathlib import Path
from writer import ListItem, TextFormatting, WriterSession, WriterTarget
from writer.core import create_document
output = str(Path("test-output/report.odt").resolve())
create_document(output)
with WriterSession(output) as session:
session.insert_text(
"Executive Summary\n\n"
"Financial Summary\n\n"
"Quarterly revenue grew 18%.\n\n"
"Action Items"
)
session.format_text(
WriterTarget(
kind="text",
text="Quarterly revenue grew 18%.",
after="Financial Summary",
before="Action Items",
),
TextFormatting(bold=True, align="center"),
)
session.insert_list(
[
ListItem(text="Confirm scope", level=0),
ListItem(text="Review output", level=0),
ListItem(text="Update packaging", level=1),
],
ordered=False,
target=WriterTarget(kind="insertion", after="Action Items"),
)from writer import patch
result = patch(
"/abs/path/report.odt",
"""
[operation]
type = replace_text
target.kind = text
target.text = Draft
new_text = Final
[operation]
type = update_table
target.kind = table
target.name = Summary
data = [["Metric", "Value"], ["Revenue", "$2M"]]
[operation]
type = replace_list
target.kind = list
target.text = Confirm scope
items = [{"text": "Approve release", "level": 0}, {"text": "Notify team", "level": 1}]
list.ordered = true
""",
mode="best_effort",
)
print(result.overall_status)from pathlib import Path
from writer import snapshot_page
result = snapshot_page(doc_path, "/tmp/page1.png", page=1, dpi=150)
print(result.file_path, result.width, result.height)
Path(result.file_path).unlink(missing_ok=True) # clean up used snapshotsUse snapshots to verify layout after formatting, list edits, image placement, or table changes.
session.export() while a document is already open in a WriterSession.export_document() when exporting an existing file without keeping a session open.get_metadata() normalizes keywords into one comma-and-space separated string.occurrence for repeated text and then getting an ambiguity error.after / before bounds when possible.align to apply only to a phrase; Writer applies paragraph alignment to the containing paragraph.items or data patch fields.session.export() or other methods after session.close()."start" and "end" paragraph alignment options require LibreOffice 26.2+.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.