Install
$ agentstack add skill-kurianoff-claude-skills-soc2-policies-policy-review ✓ 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 No
- ✓ 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.
About
Policy review skill
Presents an interactive carousel-based review board for any policy document. The user reviews each statement one at a time — approving, rejecting (with justification), rewriting manually, or requesting an AI-assisted rewrite with iterative refinement.
All policy content is HTML. The widget renders HTML directly via innerHTML. No markdown parsing happens at runtime. AI rewrites produce HTML. Manual edits save as HTML. Working copies in persistent storage are HTML.
When this skill triggers
- User uploads a policy file and asks to review it.
- User references an already-uploaded policy.
- User pastes policy text directly into the conversation.
- The soc2-policies skill hands off a policy for review.
HTML-only working copies — lazy creation
This skill operates on HTML working copies of policies.
If launched from soc2-policies (typical flow): The handoff prompt says "load from soc2:working:{id} in persistent storage. If no working copy exists, read the HTML template from references/templates/ and apply company name replacement to create one."
Claude should:
- First, try to read
soc2:working:{policy-id}from persistent storage.
If found, this is the working copy (may contain prior edits). Use it.
- If NOT found (first time reviewing this policy), read the HTML template
file from the soc2-policies skill's references/templates/ directory. Apply company name replacement ([COMPANY NAME] → actual name). This HTML becomes the working copy. The review carousel will save it to soc2:working:{policy-id} on export — do not save it now.
- Parse the HTML into sections and statements (Step 1), then render.
If launched standalone (user uploads a file directly):
- If the file is markdown, convert to HTML once. If already HTML, use as-is.
- Check
soc2:company-namein storage. If not found and the text contains
[COMPANY NAME] placeholders, ask the user. Store at soc2:company-name.
- Apply company name replacement on the HTML string.
Critical: Never write back to template files. Never store markdown in working copies. All working copies are HTML in persistent storage.
Step 1: Parse the HTML policy into sections and statements
Read the HTML working copy. Extract the hierarchical structure:
- Sections correspond to `
or` elements in the HTML. - Statements are the content blocks between headings: `
,,`,
`` elements, or groups of these that form a single reviewable item.
Build a JavaScript array for the carousel widget. Each statement's text field is an HTML string that the widget renders directly via innerHTML:
var sections = [
{ id: 'core-policy', title: 'Core policy', items: [
{ id: 1, label: "Purpose", text: "The aim of this plan is to..." },
{ id: 2, label: "Scope", text: "This policy covers...Item" }
]}
];
Parsing guidelines
- Use the document's own heading structure. Don't invent groupings.
- Each statement should be a single reviewable unit.
- Preserve the HTML exactly as-is — don't convert to plain text.
- Give each statement a short descriptive label from its heading or content.
- Sequential numeric IDs starting from 1.
Step 2: Build the interactive review carousel (COPY-AND-INJECT)
Read references/review-widget.md — it contains the COMPLETE runnable widget code with all CSS, event handlers, storage logic, sendPrompt calls, and audit logging built in. Claude's job is:
- Read the template file
- Set the injection variables at the top of the script block:
var POLICY_ID— e.g. "incident-response"var POLICY_TITLE— e.g. "Incident response plan"var COMPANY— e.g. "Acme Corp"var SECTIONS— the parsed sections array from Step 1var AI_SUGGESTION— null (or {id: N, html: "..."} for AI rewrite re-renders)var INITIAL_IDX— 0 (or specific index for AI rewrite re-renders)var INITIAL_VIEW— "statements" (or "sections")
- Copy the entire widget code into
visualize:show_widgetVERBATIM
Do NOT modify any CSS, HTML, event handlers, button labels, or storage logic. Do NOT add functions, rename classes, or change the layout.
The only things Claude changes are the injection variables. Everything else — the carousel navigation, action buttons, version history form, export function, audit logging — is fixed template code that works identically every time.
Step 3: Handle AI rewrite requests (TWO mandatory sub-steps)
When the user triggers an AI rewrite, you receive a POLICY_REWRITE_REQUEST message with the statement's current HTML text and the user's instructions.
Step 3a: Generate the rewrite as HTML
Rewrite the statement based on the user's instructions. Produce HTML output. Preserve structural elements: `, , , etc. The rewrite will be inserted directly into the widget via innerHTML`.
Step 3b: Re-render the carousel (MANDATORY — NOT OPTIONAL)
Immediately after generating the rewrite, call visualize:show_widget to render a fresh carousel with:
- All sections and statements from the original widget.
- All prior state preserved (approvals, rejections, manual edits).
state[statementId].aiSuggestionset to the rewritten HTML.currentIdxset to the correct position.
Do NOT just return plain text. ALWAYS render the full carousel.
Step 4: Handle export (save and return to dashboard)
When the user clicks "Save and return to dashboard", the widget's JavaScript:
- Reconstructs the full HTML document from all statements' current HTML.
- Builds the per-policy audit log — a chronological list of every action
taken during this review session:
- Each approve, rewrite (manual or AI), and reject is logged with timestamp,
statement label, action type, and for rewrites a summary of what changed.
- The audit log is stored at
soc2:audit:{policyId}as an array of entries. - New entries are appended to any existing audit log (from prior sessions).
- Reads version history fields from the form (version, date, description,
author, approved by) and captures them in the export data.
- Saves to persistent storage (all as HTML, await all writes):
soc2:working:{policyId}— full reconstructed HTML documentsoc2:review:{policyId}— statement decisions with full HTML textsoc2:versions:{policyId}— version history (all prior + current)soc2:audit:{policyId}— per-policy audit trail
- Sends
POLICY_REVIEW_COMPLETEwith change summary, version metadata,
AND explicit dashboard state data:
POLICY_REVIEW_COMPLETE
Policy: Acceptable Use Policy
Policy ID: acceptable-use
Version: 1.1
Date: 2026-03-23
Author: Jane Smith
Approved by: John Doe
Description: Annual review and update
REVIEW STATUS: completed
STATEMENTS TOTAL: 15
STATEMENTS REVIEWED: 15
CHANGE SUMMARY:
- 14 statements approved as-is
- 1 statement rewritten: "Confidentiality and data protection"
- 0 statements rejected
Please render the SOC 2 dashboard. When building the dashboard widget, inject
this policy's status directly into the widget state as a fallback:
{ id: "acceptable-use", status: "completed", reviewed: 15, total: 15 }
The widget should ALSO read from persistent storage for all other policies.
This dual-source approach ensures the dashboard renders correctly even if
storage writes from this widget haven't fully propagated yet.
Why dual-source matters: The sendPrompt() call creates a new widget in a new iframe. Storage writes from the previous widget may not have completed before the new widget tries to read them. By including explicit status data in the prompt, Claude can inject it directly into the dashboard widget as a known-state override. The widget merges this with whatever it reads from storage for the other 16 policies.
The version metadata MUST also be included in this message so the dashboard and export skill can use it without relying on storage reads.
Per-policy audit trail
The widget tracks actions during the review session. Each audit entry has:
timestamp— ISO datetimeaction— "approved", "rewritten (manual)", "rewritten (AI)", "rejected"statementLabel— which statement was acted ondetails— for rewrites, a brief note; for rejections, the justification
The audit log is saved to soc2:audit:{policyId} and included in the exported Word document as an "Audit trail" appendix — a chronological table that gives SOC 2 auditors a self-contained provenance record for each policy.
This skill does NOT generate Word documents. The policy-export skill handles that by reading the HTML working copy, version history, and audit log from storage.
Session persistence (survives AI rewrite roundtrips)
The widget saves review progress to soc2:review-session:{policyId} after EVERY action — approve, reject, rewrite, undo, bulk approve, discard, and version history edits. This is separate from the final soc2:review:{policyId} which is written only on export.
Why this matters: When the user triggers an AI rewrite, sendPrompt() creates a new widget in a new iframe. Without session persistence, all prior approvals/rejections would be lost. With it, the new widget loads the session state on init() and the user continues where they left off.
Storage keys:
soc2:review-session:{id}— live working state, written after every action,
contains: statement decisions, audit log, view mode, carousel position, version metadata. Cleared on final export.
soc2:review:{id}— final state, written only on "Save and return to
dashboard". This is what the dashboard and export skill read.
The session state stores:
{
decisions: {
"1": { status: "approved", text: "...", justification: "", aiSuggestion: null },
"6": { status: "rewritten", text: "new...", justification: "", aiSuggestion: null }
},
auditLog: [ { ts: "...", action: "approved", label: "Purpose", details: "" }, ... ],
viewMode: "statements",
currentIdx: 5,
versionMeta: { version: "1.0", date: "...", author: "...", ... }
}
Only non-pending decisions are stored (to keep the payload small). On load, any statement ID not in decisions stays at its default pending state.
Data format rule (enforced everywhere)
HTML is the only format for policy content in this system.
- Statement
textfields are HTML strings. state[id].text,state[id].originalText,state[id].aiSuggestionare HTML.- Working copies in
soc2:working:{id}are HTML. - AI rewrites must produce HTML, not markdown.
- Manual edit saves convert plain text to basic HTML (`` wrapping).
- The widget uses
innerHTMLfor rendering — nevertextContent+esc().
Integration with soc2-policies skill
When launched from soc2-policies (prompt includes "Policy ID" and "WORKING COPY (HTML)"), the export step persists everything to window.storage. The soc2-policies dashboard reads it back when re-rendered.
Edge cases
- Partial review: Unreviewed statements treated as approved on export.
- Very long policies: Suggest reviewing by section if >30 statements.
- Non-standard structure: Create a single "Policy statements" section.
- Multiple rounds: Each export updates the HTML working copy. Next review
starts from the latest approved HTML version.
- AI rewrite preserves state: Re-rendering the carousel after an AI rewrite
must carry forward all prior decisions.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: kurianoff
- Source: kurianoff/claude-skills-soc2-policies
- License: MIT
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.