A skill for creating, editing, and reading Hangul (Hancom Office) HWPX files with a focus on direct XML authoring . HWPX is a ZIP-based XML container (OWPML standard). For edits where format preservation is important, it directly handles OWPML XML to finely control charPr, paraPr, and table structures.
Rules for Refusing HWP Binary Requests (Required)
This skill is HWPX-only. If the user requests the writing, creation, saving, editing, filling, or generation of conversion output for a .hwp binary file, that task is not performed.
.hwpRequests to produce a result file as are refused..hwpRequests to directly modify or fill in the original are refused..hwpReading and referencing is possible, but the output is produced as only if the user explicitly permits HWPX..hwpx.- If the user requested the creation of
.hwpbut did not explicitly permit an HWPX alternative deliverable, do not arbitrarily create a.hwpxfile and treat it as completed instead. - Keep the guidance text simple: "This skill only supports HWPX, so it cannot create .hwp. If you provide a file saved as HWPX in Hangul, or if you receive the result as .hwpx, it can be processed."
Default Operation Mode (Required): Analyze attached HWPX → Restore unique XML (99% close) → Rewrite reflecting the request
If the user attached .hwpx, this skill follows the order below as the default.
- Secure reference: Use the attached HWPX as the reference document
- Slot extraction:
hwpx_slots.pycreates a JSON list of editable paragraph/cell slots - Map user values: Map new values to slot keys (
p:12,cell:0:2:1) - Structure-preserving editing:
edit_hwpx.py --slot-jsonclones the original package and modifies only the slot text - Build/validate:
edit_hwpx.pyresult orbuild_hwpx.pyresult asvalidate.pyto verify integrity - Finalization/layout warnings:
fix_namespaces.py,finalize_hwpx.py --strip-linesegarray --layout,validate.py --layoutchecks namespaces and rendering risks - Character budget/page count guard (Required):
page_guard.pychecks the character budget per paragraph/cell and the risk of page drift compared with the reference - Content completeness guard (Required):
content_guard.pychecks for leftover original text, placeholders, and missing required keywords
99% Close Restoration Criteria (Practical Checklist)
charPrIDRef,paraPrIDRef,borderFillIDRefreference system identical- Table
rowCnt,colCnt,colSpan,rowSpan,cellSz,cellMarginidentical - Paragraph order, paragraph count, and major blank line/section positions identical
- Page/margins/section (secPr) identical
- Changes are limited to the scope of the user's request (body text, values, item names, etc.)
- If the content change request is a shift in the document's overall perspective (e.g., Korean institution → US institution, specific company → another company), it fails if remnants of the previous institution name/contact person/phone number/policy name remain
Same page count (100%) is a mandatory criterion
- If the user provided a reference, the final page count of the resulting document must be the same as the reference
- If the page count appears likely to increase, first compress/summarize the text to fit the existing layout
- Without an explicit user request,
hp:p,hp:tbl,rowCnt,colCnt,pageBreak,secPris not changed validate.pyDo not treat passing only as completion. It must also passpage_guard.py와content_guard.pyas wellpage_guard.pyorcontent_guard.pyIf it fails, do not submit the result as complete; fix the cause (excessive length/structural change/source remnants/missing required keywords) and rebuild- If possible, check the final page count value based on Hangul (or the user's confirmation) and re-verify whether it matches the reference
Default execution command (when an attached reference exists)
source "$VENV"
# 1) 레퍼런스 분석 + XML 추출
python3 "$SKILL_DIR/scripts/analyze_template.py" reference.hwpx \
--extract-header /tmp/ref_header.xml \
--extract-section /tmp/ref_section.xml
# 2) 편집 가능한 슬롯 추출
python3 "$SKILL_DIR/scripts/hwpx_slots.py" reference.hwpx \
--output /tmp/reference.slots.json
# 3) 원본 양식의 문단/셀별 글자 예산 저장
python3 "$SKILL_DIR/scripts/page_guard.py" \
--reference reference.hwpx \
--write-budget /tmp/reference.budget.json \
--write-structure /tmp/reference.structure.json
# 3-1) 내용 완성도 규칙 작성
# 예: 한국 고용노동부 보도자료를 미국노동부 문서로 바꾸는 경우
cat > /tmp/content.rules.json <<'JSON'
{
"require": ["미국노동부"],
"forbid": ["고용노동부", "김영훈", "044-"],
"forbid_regex": ["○○+", "\\.\\s*\\.\\s*\\."]
}
JSON
# 4) 양식 보존 편집
python3 "$SKILL_DIR/scripts/edit_hwpx.py" reference.hwpx \
--output result.hwpx \
--slot-json values.json
# 5) 검증 + 최종화
python3 "$SKILL_DIR/scripts/validate.py" result.hwpx
python3 "$SKILL_DIR/scripts/fix_namespaces.py" result.hwpx
python3 "$SKILL_DIR/scripts/finalize_hwpx.py" result.hwpx --strip-linesegarray --layout
python3 "$SKILL_DIR/scripts/validate.py" result.hwpx --layout
# 6) 글자 예산 + 쪽수 드리프트 가드 (필수)
python3 "$SKILL_DIR/scripts/page_guard.py" \
--reference reference.hwpx \
--output result.hwpx \
--budget-profile /tmp/reference.budget.json \
--structure-profile /tmp/reference.structure.json \
--no-strict-paragraph-budget \
--skip-text-drift \
--allow-empty-fill
# 7) 내용 완성도 가드 (필수)
python3 "$SKILL_DIR/scripts/content_guard.py" result.hwpx \
--rules /tmp/content.rules.json
# 문서 전체 관점 전환/전면 재작성인 경우 원본 긴 문장 잔존율도 제한
python3 "$SKILL_DIR/scripts/content_guard.py" result.hwpx \
--reference reference.hwpx \
--rules /tmp/content.rules.json \
--max-unchanged-ratio 0.35
# 공문서라면 날짜/시간/금액/붙임 표기도 점검
python3 "$SKILL_DIR/scripts/gonmun_lint.py" --hwpx result.hwpx --format text
hwpx_slots.pyexposes actually editable paragraphs and table cells as slots, excluding container paragraphs that hold tables/images/text boxes. The user only needs to put values into the slot keys.
edit_hwpx.py는 mimetype, content.hpf, header.xml, settings.xml, Preview/*, BinData/*is copied verbatim from the original. The modification target is basically only Contents/section0.xml the slot text inside. Therefore, if the user provided an existing form and only text/cell filling is needed, build_hwpx.pythan edit_hwpx.py --slot-jsonis preferred.
Packaging must not use ordinary ZIP recompression. Copy the original ZIP's local headers and compressed data as-is, and replace only the target entries. For unmodified entries, the CRC, compressed size, file size, flag bits, compression method, date, creating system, and external/internal attributes must be the same as the original.
edit_hwpx.py Usage rules
# 전체 텍스트 치환
python3 "$SKILL_DIR/scripts/edit_hwpx.py" form.hwpx \
-o out.hwpx \
--replace "기존문구=새문구"
# JSON 매핑 치환
python3 "$SKILL_DIR/scripts/edit_hwpx.py" form.hwpx \
-o out.hwpx \
--replace-json values.json
# 표 셀 채우기: row,col은 0부터 시작, table 생략 시 첫 번째 표
python3 "$SKILL_DIR/scripts/edit_hwpx.py" form.hwpx \
-o out.hwpx \
--cell "0,2,1=테스트 법인"
# 본문 문단 재작성: 문장 품질을 우선하고 원본 문단 예산 이하로만 제한
python3 "$SKILL_DIR/scripts/edit_hwpx.py" form.hwpx \
-o out.hwpx \
--paragraph-json paragraphs.json
# 권장: 슬롯 키 기반 채우기
python3 "$SKILL_DIR/scripts/hwpx_slots.py" form.hwpx -o slots.json
python3 "$SKILL_DIR/scripts/edit_hwpx.py" form.hwpx \
-o out.hwpx \
--slot-json values.json
--replaceis first replaced within individualhp:tto preserve run formatting as much as possible.- If the replacement target is split across multiple runs, merge at the paragraph level, but do not unconditionally use the first run.
header.xml의charPrCheck and select the main run that has less emphasis such as bold/color/underline and whose character height is closest to the most-used non-emphasized text height within that paragraph. --cellpreserves the formatting of the first run within the correspondinghp:tcand inserts only the text.--paragraph/--paragraph-jsonis for rewriting practical body text. Rather than mechanically splitting existing sentences across multiple runs, insert natural prose into one main text run of the paragraph. The main run is chosen not just by character count but bycharPrIDRef's bold/color/underline and the dominant body height within the paragraph.hh:strikeout shape="3D"Values such as these are attached to ordinary body charPr in some HWPX files. If actual strikethrough rendering is not clear, do not use it alone as a criterion for emphasis/exclusion.--slot/--slot-jsonis the default interface. Slot keys use only those generated byhwpx_slots.py.--paragraphThe target is directlyhp:run/hp:tIt must be an actual text paragraph with . Container paragraphs that contain tables/images/text boxes are not modified directly. Modifying a container causes the internal text and the new text to be rendered overlapping.- Like an empty input cell,
hp:runexists buthp:tis absent, add onlyhp:tinside the existing run. hp:tinsidehp:fwSpace,hp:lineBreak, and other child controls are never deleted. Even when clearing the text, keep the child tags and clear onlytext/tail.- Of a paragraph whose text was changed,
hp:linesegarrayis removed. This tag is a paragraph line layout cache, so if it is preserved after modifying the body, Hancom may issue a corruption/tampering warning. hp:tbl,hp:tr,hp:tc,cellAddr,cellSpan,cellSz,cellMargin,borderFillIDRefis not changed.
Practical input quality rules
For body paragraphs, spelling and spacing take priority. To preserve the form, 정부는현장점검과공개보고...do not remove spaces or cut sentences in the middle as in .
- Body rewriting uses
--paragraph-json. - When choosing a paragraph index,
analyze_template.pyor from the text extraction result, choose the internal actual text paragraph, not the container paragraph. For example, modify the institution name/title paragraph inside a table cell, not the entire top table paragraph. - The body is rewritten shorter than or equal to the original paragraph's non-space character count. Do not force-pad it to exactly the same character count as the original.
- Strictly apply the exact length/cell budget only to fields such as names, dates, phone numbers, job titles, amounts, and short table cells.
- Excessively long Korean strings without spaces
edit_hwpx.pyfail in the default quality guard of . If it is not a normal sentence, do not submit the result. - If the original run's bold/emphasis gets buried in the middle of a new body sentence, it is a failure. The body is put into a single main run of the
--paragraphfamily, and only intended emphasis such as titles/subheadings retains the original style. - If the font size looks different from the original,
header.xmlfirst compare whether changed, or whether the new text went into a differentcharPrIDRef.header.xmlEven if is the same, choosing a 12pt parenthesis/footnote run instead of 14pt body text changes the displayed typeface.
Character budget guard rules
page_guard.py --write-budgetcalculates the following budget from the original form.
- Normal paragraph: directly
hp:run/hp:tthe non-space character count of the text - Table cell: the larger of the non-space character count of the entire
hp:ttext inside the cell and the estimated capacity based on cell width/font size - Empty input cell: using the existing cell width and the first run's
charPrIDReffont size, calculate a conservative capacity
When validating the result, --budget-profilemust be used. For practical body rewriting results, --no-strict-paragraph-budget --skip-text-driftis also used to check not for an exact match of the original character count but for being within budget and preserving structure. Table cells and short input fields fail if they exceed the budget. Paragraphs inside a cell are checked against the cell budget, so empty-cell input is possible.
Full structure fingerprint guard rules
page_guard.py --write-structurestores the following in the original form.
- ZIP package file list and order
- compression method, date, creating system, version, flag bits, external/internal attributes of each entry
- SHA-256 hash of binary/attached files
- tag names, attributes, hierarchy, child order of all XML files
hp:tdirect text of andhp:ttail text of child controls is treated as input and excludedhp:tchild control tags/order such as insidehp:fwSpace,hp:lineBreakare included in the structure fingerprinthp:linesegarrayis treated as a line placement cache and excluded from the structure fingerprint
When verifying results, --structure-profilemust be used. hp:t text input and hp:linesegarray If the tag/attribute/file/image/meta structure changes other than cache removal, it fails. In particular, hp:t If an internal control is deleted, the possibility of corruption is high, so it must fail.
ZIP entry's flag_bitsmust also be the same as the original. Python zipfile.writestr()If you rewrite the entire entry with flag_bitsand the compressed bytes may change, so do not use it. Always replace only the target entry using the raw ZIP copy method.
edit_hwpx.pybasically checks the pre-input character count budget and basic sentence quality. --paragraph value must be less than or equal to the original paragraph budget, and --cell value must not exceed the original cell budget. --replaceIf the new value is longer than the existing slot, it fails before writing. --allow-over-budgetis used only when the user explicitly takes the risk.
Content completeness guard rules
content_guard.pychecks whether the result text matches the work intent regardless of whether the document opens. In the following cases, a rule file must be created and executed.
- When changing the document's institution/country/company/brand: put the previous name, previous person in charge, previous phone number, and previous policy name into
forbid. - When filling form placeholders:
○○,{{name}},... . .prohibit leftover placeholders such as . - When key terms from the new document perspective must be present: put the new institution name, new corporate name, new department in charge, etc. into
require. - In documents with multiple attachments/references/person-in-charge tables, do not change only the front part; run
content_guard.pyon the entire text extraction result. - For full rewrite requests such as "change the content completely" or "change it so it looks like it was written in the US," use
--reference와--max-unchanged-ratiotogether. If many long sentences from the original remain, it fails even if the structure is normal.
Example:
{
"require": ["미국노동부", "좋은 일자리"],
"forbid": ["고용노동부", "김영훈", "최영범", "장지훈", "044-"],
"forbid_regex": ["○○+", "\\(\\s*\\.\\s*\\.\\s*\\.\\s*\\)"]
}
content_guard.pyIf fails, do not submit the result as complete no matter how normal the structure is. Review the failure list and further modify the remaining slots, or expand the scope of the full document rewrite and build again.
Environment
# SKILL_DIR는 이 SKILL.md가 위치한 디렉토리의 절대 경로로 설정
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" # 스크립트 내에서
# 또는 Claude Code가 자동으로 주입하는 base directory 경로를 사용
# Python 가상환경 (프로젝트에 맞게 설정)
VENV="<프로젝트>/.venv/bin/activate"
For every Python execution:
# 프로젝트의 .venv를 활성화 (pip install lxml 필요)
source "$VENV"
Directory structure
.claude/skills/hwpx/
├── SKILL.md # 이 파일
├── scripts/
│ ├── office/
│ │ ├── unpack.py # HWPX → 디렉토리 (기본 XML 바이트 보존, --pretty는 검사 전용)
│ │ └── pack.py # 디렉토리 → HWPX
│ ├── build_hwpx.py # 템플릿 + XML → .hwpx 조립 (핵심)
│ ├── edit_hwpx.py # 원본 패키지 보존 + 텍스트/셀 최소 수정
│ ├── analyze_template.py # HWPX 심층 분석 (레퍼런스 기반 생성용)
│ ├── validate.py # HWPX 구조 검증
│ ├── fix_namespaces.py # 표준 네임스페이스 프리픽스/header itemCnt 보정
│ ├── finalize_hwpx.py # 줄 배치 캐시 제거 + 레이아웃 위험 경고
│ ├── page_guard.py # 레퍼런스 대비 페이지 드리프트 위험 검사
│ ├── content_guard.py # 원문 잔재/placeholder/필수 키워드 검사
│ ├── gonmun_lint.py # 공문서 날짜/시간/금액/붙임 표기 검수
│ └── text_extract.py # 텍스트 추출
├── templates/
│ ├── base/ # 베이스 템플릿 (Skeleton 기반)
│ │ ├── mimetype, META-INF/*, version.xml, settings.xml, Preview/*
│ │ └── Contents/ (header.xml, section0.xml, content.hpf)
│ ├── gonmun/ # 공문 오버레이 (header.xml, section0.xml)
│ ├── report/ # 보고서 오버레이
│ ├── minutes/ # 회의록 오버레이
│ └── proposal/ # 제안서/사업개요 오버레이 (색상 헤더바, 번호 배지)
└── references/
└── hwpx-format.md # OWPML XML 요소 레퍼런스
Workflow 1: XML-first document generation (secondary workflow, only when there is no reference file)
Flow
- Select template (base/gonmun/report/minutes/proposal)
- Write section0.xml (body content)
- (Optional) Modify header.xml (when new styles need to be added)
- Build with build_hwpx.py
- Validate with validate.py
Principle: If the user provides a reference HWPX, use the "Default behavior mode (reference restoration first)" above instead of this workflow.
Basic usage
source "$VENV"
# 빈 문서 (base 템플릿)
python3 "$SKILL_DIR/scripts/build_hwpx.py" --output result.hwpx
# 템플릿 사용
python3 "$SKILL_DIR/scripts/build_hwpx.py" --template gonmun --output result.hwpx
# 커스텀 section0.xml 오버라이드
python3 "$SKILL_DIR/scripts/build_hwpx.py" --template gonmun --section my_section0.xml --output result.hwpx
# header도 오버라이드
python3 "$SKILL_DIR/scripts/build_hwpx.py" --header my_header.xml --section my_section0.xml --output result.hwpx
# 메타데이터 설정
python3 "$SKILL_DIR/scripts/build_hwpx.py" --template report --section my.xml \
--title "제목" --creator "작성자" --output result.hwpx
Practical pattern: Write section0.xml inline → build
# 1. section0.xml을 임시파일로 작성
SECTION=$(mktemp /tmp/section0_XXXX.xml)
cat > "$SECTION" << 'XMLEOF'
<?xml version='1.0' encoding='UTF-8'?>
<hs:sec xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph"
xmlns:hs="http://www.hancom.co.kr/hwpml/2011/section">
<!-- secPr 포함 첫 문단 (base/section0.xml에서 복사) -->
<!-- ... -->
<hp:p id="1000000002" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="0">
<hp:t>본문 내용</hp:t>
</hp:run>
</hp:p>
</hs:sec>
XMLEOF
# 2. 빌드
python3 "$SKILL_DIR/scripts/build_hwpx.py" --section "$SECTION" --output result.hwpx
# 3. 정리
rm -f "$SECTION"
section0.xml writing guide
Required structure
In the first paragraph of section0.xml (<hp:p>)의 첫 런(<hp:run>) must <hp:secPr>과 <hp:colPr> include:
<hp:p id="1000000001" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="0">
<hp:secPr ...>
<!-- 페이지 크기, 여백, 각주/미주 설정 등 -->
</hp:secPr>
<hp:ctrl>
<hp:colPr id="" type="NEWSPAPER" layout="LEFT" colCount="1" sameSz="1" sameGap="0"/>
</hp:ctrl>
</hp:run>
<hp:run charPrIDRef="0"><hp:t/></hp:run>
</hp:p>
Tip: templates/base/Contents/section0.xml You can simply copy the first paragraph of .
Paragraph
<hp:p id="고유ID" paraPrIDRef="문단스타일ID" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="글자스타일ID">
<hp:t>텍스트 내용</hp:t>
</hp:run>
</hp:p>
빈 줄
<hp:p id="고유ID" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="0"><hp:t/></hp:run>
</hp:p>
Mixed-format runs (multiple styles in one paragraph)
<hp:p id="고유ID" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="0"><hp:t>일반 텍스트 </hp:t></hp:run>
<hp:run charPrIDRef="7"><hp:t>볼드 텍스트</hp:t></hp:run>
<hp:run charPrIDRef="0"><hp:t> 다시 일반</hp:t></hp:run>
</hp:p>
How to create tables
<hp:p id="고유ID" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="0">
<hp:tbl id="고유ID" zOrder="0" numberingType="TABLE" textWrap="TOP_AND_BOTTOM"
textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" pageBreak="CELL"
repeatHeader="0" rowCnt="행수" colCnt="열수" cellSpacing="0"
borderFillIDRef="3" noAdjust="0">
<hp:sz width="42520" widthRelTo="ABSOLUTE" height="전체높이" heightRelTo="ABSOLUTE" protect="0"/>
<hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0"
holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="COLUMN" vertAlign="TOP"
horzAlign="LEFT" vertOffset="0" horzOffset="0"/>
<hp:outMargin left="0" right="0" top="0" bottom="0"/>
<hp:inMargin left="0" right="0" top="0" bottom="0"/>
<hp:tr>
<hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="1" borderFillIDRef="4">
<hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER"
linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0"
hasTextRef="0" hasNumRef="0">
<hp:p paraPrIDRef="21" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0" id="고유ID">
<hp:run charPrIDRef="9"><hp:t>헤더 셀</hp:t></hp:run>
</hp:p>
</hp:subList>
<hp:cellAddr colAddr="0" rowAddr="0"/>
<hp:cellSpan colSpan="1" rowSpan="1"/>
<hp:cellSz width="열너비" height="행높이"/>
<hp:cellMargin left="0" right="0" top="0" bottom="0"/>
</hp:tc>
<!-- 나머지 셀... -->
</hp:tr>
</hp:tbl>
</hp:run>
</hp:p>
Table size calculation
- A4 body width: 42520 HWPUNIT = 59528 (paper) - 8504×2 (left/right margins)
- Sum of column widths = body width (42520)
- Example: 3 equal columns → 14173 + 14173 + 14174 = 42520
- Example: 2 columns (label:content = 1:4) → 8504 + 34016 = 42520
- Row height: usually 2400~3600 HWPUNIT per cell
ID rules
- Paragraph id:
1000000001increasing sequentially from - 표 id:
1000000099It is recommended to use separate ranges such as - All ids must be unique within the document
header.xml modification guide
How to add a custom style
templates/base/Contents/header.xmlcopy- Add the necessary charPr/paraPr/borderFill
- For each group,
itemCntupdate the attributes
Example of adding charPr (bold 14pt)
<hh:charPr id="8" height="1400" textColor="#000000" shadeColor="none"
useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="2">
<hh:fontRef hangul="1" latin="1" hanja="1" japanese="1" other="1" symbol="1" user="1"/>
<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
<hh:bold/>
<hh:underline type="NONE" shape="SOLID" color="#000000"/>
<hh:strikeout shape="NONE" color="#000000"/>
<hh:outline type="NONE"/>
<hh:shadow type="NONE" color="#C0C0C0" offsetX="10" offsetY="10"/>
</hh:charPr>
Font reference system
fontRefThe value isfontfacesthe font id defined inhangul="0"→ Hamchorom Dotum (Gothic)hangul="1"→ Hamchorom Batang (Myeongjo)- Set identically for all 7 languages
Cautions when adding paraPr
- Must
hp:switchinclude structure (hp:case+hp:default) hp:case와hp:defaultThe values of are usually the same (or default is 2x)borderFillIDRef="2"maintain
Style ID map by template
base (default)
| ID | Type | Description |
|---|---|---|
| charPr 0 | Text | 10pt Hamchorom Batang, default |
| charPr 1 | Text | 10pt Hamchorom Dotum |
| charPr 2~6 | Text | Skeleton default style |
| paraPr 0 | Paragraph | JUSTIFY, 160% line spacing |
| paraPr 1~19 | Paragraph | Skeleton default (outline, footnotes, etc.) |
| borderFill 1 | Border | None (page border) |
| borderFill 2 | Border | None + transparent background (for reference) |
gonmun (official document) — base + additions
| ID | Type | Description |
|---|---|---|
| charPr 7 | Text | 22pt bold Hamchorom Batang (organization name/title) |
| charPr 8 | Text | 16pt bold Hamchorom Batang (signer) |
| charPr 9 | Text | 8pt Hamchorom Batang (bottom contact info) |
| charPr 10 | Text | 10pt bold Hamchorom Batang (table header) |
| paraPr 20 | Paragraph | CENTER, 160% line spacing |
| paraPr 21 | Paragraph | CENTER, 130% (표 셀) |
| paraPr 22 | Paragraph | JUSTIFY, 130% (표 셀) |
| borderFill 3 | Border | SOLID 0.12mm 4면 |
| borderFill 4 | Border | SOLID 0.12mm + #D6DCE4 background |
report — base + additions
| ID | Type | Description |
|---|---|---|
| charPr 7 | Text | 20pt bold (document title) |
| charPr 8 | Text | 14pt bold (subheading) |
| charPr 9 | Text | 10pt bold (table header) |
| charPr 10 | Text | 10pt bold+underline (emphasized text) |
| charPr 11 | Text | 9pt Hamchorom Batang (small/footnote) |
| charPr 12 | Text | 16pt bold Hamchorom Batang (one-line title) |
| charPr 13 | Text | 12pt bold Hamchorom Dotum (section header) |
| paraPr 20~22 | Paragraph | CENTER/JUSTIFY variants |
| paraPr 23 | Paragraph | RIGHT aligned, 160% line spacing |
| paraPr 24 | Paragraph | JUSTIFY, left 600 (□ checklist item indent) |
| paraPr 25 | Paragraph | JUSTIFY, left 1200 (sub-item ①②③ indent) |
| paraPr 26 | Paragraph | JUSTIFY, left 1800 (deep sub-item - indent) |
| paraPr 27 | Paragraph | LEFT, top/bottom border lines (for section header), prev 400 |
| borderFill 3 | Border | SOLID 0.12mm 4면 |
| borderFill 4 | Border | SOLID 0.12mm + #DAEEF3 background |
| borderFill 5 | Border | Top 0.4mm thick line + bottom 0.12mm thin line (section header) |
Indentation rules: Must use paraPr's left margin, not space characters. □ items use paraPr 24, sub-items ①②③ use paraPr 25, deep - items use paraPr 26.
Section header rules: paraPr 27 + charPr 13 combination. Paragraph border (borderFillIDRef="5") automatically displays top thick line + bottom thin line.
minutes — base + additions
| ID | Type | Description |
|---|---|---|
| charPr 7 | Text | 18pt bold (title) |
| charPr 8 | Text | 12pt bold (section label) |
| charPr 9 | Text | 10pt bold (table header) |
| paraPr 20~22 | Paragraph | CENTER/JUSTIFY variants |
| borderFill 3 | Border | SOLID 0.12mm 4면 |
| borderFill 4 | Border | SOLID 0.12mm + #E2EFDA background |
proposal — base + additions
For official documents requiring visual distinction. Color background header bars and number badges implemented with table-based layout.
| ID | Type | Description |
|---|---|---|
| charPr 7 | Text | 20pt bold Hamchorom Batang (document title) |
| charPr 8 | Text | 14pt bold Hamchorom Batang (subheading) |
| charPr 9 | Text | 10pt bold Hamchorom Batang (table header) |
| charPr 10 | Text | 14pt bold white Hamchorom Dotum (major item number, green background) |
| charPr 11 | Text | 11pt bold white Hamchorom Dotum (minor item number, blue background) |
| paraPr 20 | Paragraph | CENTER, 160% line spacing |
| paraPr 21 | Paragraph | CENTER, 130% (표 셀) |
| paraPr 22 | Paragraph | JUSTIFY, 130% (표 셀) |
| borderFill 3 | Border | SOLID 0.12mm 4면 |
| borderFill 4 | Border | SOLID 0.12mm + #DAEEF3 background |
| borderFill 5 | Border | Olive green background #7B8B3D (major item number cell) |
| borderFill 6 | Border | Light gray background #F2F2F2 + gray border (major item title cell) |
| borderFill 7 | Border | Blue background #4472C4 (minor item number badge) |
| borderFill 8 | Border | Bottom border only #D0D0D0 (minor item title area) |
proposal layout pattern
Major item header (2-cell table: number + title):
<!-- borderFillIDRef="5" + charPrIDRef="10" → 녹색배경 흰색 로마숫자 -->
<!-- borderFillIDRef="6" + charPrIDRef="8" → 회색배경 검정 볼드 제목 -->
Minor item header (2-cell table: number badge + title):
<!-- borderFillIDRef="7" + charPrIDRef="11" → 파란배경 흰색 아라비아숫자 -->
<!-- borderFillIDRef="8" + charPrIDRef="8" → 하단선만 검정 볼드 제목 -->
Workflow 2: Editing an existing document (unpack → Edit → pack)
source "$VENV"
# 1. HWPX → 디렉토리 (기본값은 XML 바이트 보존)
python3 "$SKILL_DIR/scripts/office/unpack.py" document.hwpx ./unpacked/
# 2. XML 직접 편집 (Claude가 Read/Edit 도구로)
# 본문: ./unpacked/Contents/section0.xml
# 스타일: ./unpacked/Contents/header.xml
# 3. 다시 HWPX로 패키징
python3 "$SKILL_DIR/scripts/office/pack.py" ./unpacked/ edited.hwpx
# 4. 검증
python3 "$SKILL_DIR/scripts/validate.py" edited.hwpx
unpack.py --prettyis used only when a person wants to inspect the XML in a readable way. HWPX's hp:tmay have mixed content in which text and child controls such as hp:fwSpace, hp:lineBreak are mixed, and line breaks/spaces inserted by pretty-print may be rendered as actual text by Hancom. Therefore, --prettydo not use the directory unpacked with pack.py as input again.
Workflow 3: Reading/text extraction
source "$VENV"
# 순수 텍스트
python3 "$SKILL_DIR/scripts/text_extract.py" document.hwpx
# 테이블 포함
python3 "$SKILL_DIR/scripts/text_extract.py" document.hwpx --include-tables
# 마크다운 형식
python3 "$SKILL_DIR/scripts/text_extract.py" document.hwpx --format markdown
Python API
from hwpx import TextExtractor
with TextExtractor("document.hwpx") as ext:
text = ext.extract_text(include_nested=True, object_behavior="nested")
print(text)
Workflow 4: Validation
source "$VENV"
python3 "$SKILL_DIR/scripts/validate.py" document.hwpx
Validation items: ZIP validity, required files existence, mimetype content/location/compression method, XML well-formedness
Workflow 5: Reference-based document generation (applied by default when an attached HWPX exists)
A workflow that analyzes a user-provided HWPX file and generates a document with the same layout. In this skill, if an attached reference exists, this workflow is used by default.
Flow
- Analysis —
analyze_template.pyin-depth analysis of the reference document with - Extract header.xml — use the reference's style definitions as they are
- Write section0.xml — write new content following the structure of the analysis result
- Build — build with the extracted header.xml + new section0.xml
- Validation —
validate.py - Page count guard —
page_guard.py(revise again if it fails)
Usage
source "$VENV"
# 1. 심층 분석 (구조 청사진 출력)
python3 "$SKILL_DIR/scripts/analyze_template.py" reference.hwpx
# 2. header.xml과 section0.xml을 추출하여 참고용으로 보관
python3 "$SKILL_DIR/scripts/analyze_template.py" reference.hwpx \
--extract-header /tmp/ref_header.xml \
--extract-section /tmp/ref_section.xml
# 3. 분석 결과를 보고 새 section0.xml 작성
# - 동일한 charPrIDRef, paraPrIDRef 사용
# - 동일한 테이블 구조 (열 수, 열 너비, 행 수, rowSpan/colSpan)
# - 동일한 borderFillIDRef, cellMargin
# 4. 추출한 header.xml + 새 section0.xml로 빌드
python3 "$SKILL_DIR/scripts/build_hwpx.py" \
--header /tmp/ref_header.xml \
--section /tmp/new_section0.xml \
--output result.hwpx
# 5. 검증
python3 "$SKILL_DIR/scripts/validate.py" result.hwpx
# 6. 쪽수 드리프트 가드 (필수)
python3 "$SKILL_DIR/scripts/page_guard.py" \
--reference reference.hwpx \
--output result.hwpx
Analysis output items
| Item | Description |
|---|---|
| Font definition | hangul/latin font mapping |
| borderFill | Border type/thickness + background color (details for each side) |
| charPr | Font size (pt), font name, color, bold/italic/underline/strikethrough, fontRef |
| paraPr | Alignment, line spacing, margins (left/right/prev/next/intent), heading, borderFillIDRef |
| Document structure | Page size, margins, page borders, body width |
| Body details | id/paraPr/charPr of all paragraphs + text content |
| Table details | rows×columns, column width array, per-cell span/margin/borderFill/vertAlign + content |
Core principles
- Use charPrIDRef/paraPrIDRef as is: Do not change the style IDs of the extracted header.xml
- Sum of column widths = body width: Duplicate the column width array from the analysis results as is
- Maintain rowSpan/colSpan patterns: Accurately reproduce the analyzed cell merge structure
- Preserve cellMargin: Apply the analyzed cell margin values identically
- Do not increase pages: Do not increase the resulting page count without explicit user approval
- Replacement-first editing: Prioritize replacing existing text nodes over adding new paragraphs/tables
Script summary
| Script | Purpose |
|---|---|
scripts/build_hwpx.py |
Key — Template + XML → HWPX assembly |
scripts/analyze_template.py |
HWPX in-depth analysis (blueprint for reference-based generation) |
scripts/office/unpack.py |
HWPX → directory (preserve raw XML bytes, --prettyis for inspection only) |
scripts/office/pack.py |
Directory → HWPX (mimetype first) |
scripts/validate.py |
Validate HWPX file structure |
scripts/fix_namespaces.py |
Standard namespace prefixes and header itemCnt correction |
scripts/finalize_hwpx.py |
Remove line layout cache, layout risk warning, Windows Hancom open check |
scripts/page_guard.py |
Check page drift risk against reference (required gate) |
scripts/content_guard.py |
Check for original text remnants, placeholders, and missing required keywords |
scripts/gonmun_lint.py |
Review official document date/time/amount/attachment notation |
scripts/text_extract.py |
HWPX text extraction |
Unit conversion
| 값 | HWPUNIT | Meaning |
|---|---|---|
| 1pt | 100 | Base unit |
| 10pt | 1000 | Base font size |
| 1mm | 283.5 | Millimeters |
| 1cm | 2835 | Centimeters |
| A4 폭 | 59528 | 210mm |
| A4 height | 84186 | 297mm |
| Left/right margins | 8504 | 30mm |
| Body width | 42520 | 150mm (A4 - left/right margins) |
Critical Rules
- HWPX only supported / HWP creation refused:
.hwp(binary) file creation, generation, saving, direct editing, and filling requests are refused. Only when the user explicitly permits a HWPX alternative output, work with.hwpxWhen the user provides a.hwpfile, In Hancom Office,.hwpxsave again asguide them to do so. (File → Save As → File type: HWPX) - secPr required: The first run of the first paragraph in section0.xml must include secPr + colPr
- mimetype order: When packaging HWPX, mimetype is the first ZIP entry, ZIP_STORED
- Preserve namespaces: When editing XML,
hp:,hs:,hh:,hc:keep the prefix - itemCnt consistency: charProperties/paraProperties/borderFills itemCnt in header.xml matches the actual number of children
- ID reference consistency: charPrIDRef/paraPrIDRef in section0.xml matches the header.xml definition
- use venv: of the project
.venv/bin/python3(lxml package required) - Validation: after generation, always
validate.pyto verify integrity - Reference: for detailed XML structure, see
$SKILL_DIR/references/hwpx-format.mdrefer to - build_hwpx.py preferred: for creating new documents, use build_hwpx.py (avoid calling the python-hwpx API directly)
- 빈 줄:
<hp:t/>use (self-closing tag) - reference-first enforcement: when the user attaches an HWPX, always
analyze_template.py+ restore/rewrite based on the extracted XML - do not use the examples folder: during work,
.claude/skills/hwpx/examples/*files must not be used for reading/reference/copying - page count must be identical: in reference-based work, keep the final result's page count identical to the reference
- no unauthorized page increase: do not make structural changes that cause page count increases without the user's explicit request/approval
- restrict structural changes: unless requested by the user, do not add/delete/split/merge paragraphs or tables (replacement-focused editing)
- page_guard must pass:
validate.pyand separately frompage_guard.pymust be passed to mark as complete