Install
$ agentstack add skill-jakeintech-claude-ios-skills-ios-site ✓ 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 Used
- ● Shell / process execution Used
- ✓ 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.
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
iOS Marketing/Legal Site
Scaffolds a GitHub Pages site with four pages:
index.html— branded landing (dark background, brand accent color)privacy-policy.html— neutral light styling, derived fromappstore/privacy-labels.jsonterms-of-service.html— neutral, derived fromproducts.yml+ brand booksupport.html— neutral FAQ derived from brand book + key features
Reference
Load reference.md before starting for gh CLI commands, Pages API, CNAME/DNS notes, and troubleshooting.
Modes
init— First-time scaffold: create local repo dir, generate content, create GitHub repo, push, enable Pages, write URLs back toappstore/app-info.json.update— Regenerate content in existing repo, commit + push only if changed.plan(default) — Show what would change, no writes or network mutations.
Flags
--in-repo— scaffold into/site/instead of a sibling repo. Skips GitHub repo creation and Pages enablement; user manages deployment themselves.--domain DOMAIN— write aCNAMEfile and print DNS records to add at the user's registrar.
Config Files
| File | Required | Source | |---|---|---| | docs/product-vision/00-product-bounds.md | Yes | ios-prd | | appstore/app-info.json | Yes | manual | | appstore/privacy-labels.json | Yes | ios-privacy | | appstore/age-rating.json | Yes | ios-privacy | | appstore/products.yml | Optional | storekit-iac (for ToS pricing) | | appstore/listing.json | Optional | ios-store-listing (for landing hero reuse) | | appstore/review-notes.json | Optional | ios-privacy (for support_email fallback) | | CLAUDE.md | Yes | project | | site/site.config.json | Optional | manual — see below |
site/site.config.json (optional overrides)
{
"repo_name": "udana-site",
"github_user": "jakeintech",
"developer_name": "Jake Williams",
"support_email": "info@jakeawilliams.com",
"app_store_url": "https://apps.apple.com/app/id6762299975",
"custom_domain": null,
"faq_overrides": []
}
All fields optional. Missing fields are derived:
repo_namedefault:-sitegithub_userdefault:gh api user -q .loginsupport_emaildefault:review-notes.json.contactEmailapp_store_urldefault:https://apps.apple.com/app/id{app_id}from app-info.json
Process
1. Validate
for f in "docs/product-vision/00-product-bounds.md" "appstore/app-info.json" "appstore/privacy-labels.json" "appstore/age-rating.json" "CLAUDE.md"; do
test -f "$f" || { echo "MISSING: $f"; exit 1; }
done
# Check gh CLI for init mode
if [ "$MODE" = "init" ]; then
gh auth status 2>&1 || { echo "gh CLI not authenticated. Run: gh auth login"; exit 1; }
fi
2. Resolve config
import json, os, subprocess
from datetime import date
app_info = json.load(open("appstore/app-info.json"))
site_config = {}
if os.path.exists("site/site.config.json"):
site_config = json.load(open("site/site.config.json"))
def slug(s):
return "".join(c.lower() if c.isalnum() else "-" for c in s).strip("-")
repo_name = site_config.get("repo_name") or f"{slug(app_info['name'])}-site"
# GitHub user
gh_user = site_config.get("github_user")
if not gh_user:
gh_user = subprocess.check_output(["gh", "api", "user", "-q", ".login"]).decode().strip()
# Support email
support_email = site_config.get("support_email")
if not support_email:
try:
rn = json.load(open("appstore/review-notes.json"))
support_email = rn.get("contactEmail", "")
except FileNotFoundError:
support_email = ""
assert support_email, "support_email required — set in site.config.json or review-notes.json"
# App Store URL
app_store_url = site_config.get("app_store_url") or f"https://apps.apple.com/app/id{app_info['app_id']}"
# Developer name
developer = site_config.get("developer_name", "")
# Site URL
custom_domain = site_config.get("custom_domain")
site_url = f"https://{custom_domain}" if custom_domain else f"https://{gh_user}.github.io/{repo_name}"
3. Dispatch subagents in parallel
One message, two Agent calls:
- brand-designer per
agents/brand-designer.md— pass brandbook, claudemd, listing (if exists), appname, appstore_url. Returnslanding_contentJSON. - legal-writer per
agents/legal-writer.md— pass appname, developername, supportemail, privacylabels, agerating, products (if exists), claudemd, brandbook, lastupdated. Returnslegal_contentJSON.
Retry any failed agent ONCE with stricter JSON-only prompt. Second failure → abort.
4. Render templates
For each template in templates/:
import re
from datetime import date
def render(template_path, context):
with open(template_path) as f:
html = f.read()
for key, value in context.items():
html = html.replace("{{" + key + "}}", str(value))
# Validate no leftover placeholders
leftovers = re.findall(r"\{\{[^}]+\}\}", html)
if leftovers:
raise RuntimeError(f"Unresolved placeholders in {template_path}: {leftovers}")
return html
# Build stats HTML
stats_html = "\n".join(
f'{s["number"]}{s["label"]}'
for s in landing["stats"]
)
# Build features HTML
features_html = "\n".join(
f'{f["title"]}{f["body"]}'
for f in landing["features"]
)
index_ctx = {
"app_name": app_info["name"],
"tagline": landing["tagline"],
"meta_description": landing["meta_description"],
"palette_bg": landing["palette"]["bg"],
"palette_accent": landing["palette"]["accent"],
"palette_muted": landing["palette"]["muted"],
"palette_ink": landing["palette"]["ink"],
"hero": landing["hero"],
"stats_html": stats_html,
"features_html": features_html,
"cta_url": landing["cta"]["url"],
"cta_text": landing["cta"]["text"],
"year": date.today().year,
"developer": developer,
}
today_str = date.today().strftime("%B %-d, %Y")
privacy_ctx = {
"app_name": app_info["name"],
"last_updated": today_str,
"privacy_body_html": legal["privacy_body_html"],
}
terms_ctx = {
"app_name": app_info["name"],
"last_updated": today_str,
"terms_body_html": legal["terms_body_html"],
}
support_ctx = {
"app_name": app_info["name"],
"faq_html": legal["faq_html"],
"support_email": support_email,
}
Call render() for each template with its context.
5. Decide target directory
if "--in-repo" in args:
site_dir = "site"
else:
site_dir = os.path.expanduser(f"~/Documents/GitHub/{repo_name}")
6. init mode
# Create dir
mkdir -p "$SITE_DIR"
# In non-empty existing dir: stop
if [ -n "$(ls -A "$SITE_DIR" 2>/dev/null)" ]; then
echo "ERROR: $SITE_DIR exists and is non-empty"
exit 1
fi
# Write rendered files (each HTML file from step 4)
# Write CNAME if custom_domain
if [ -n "$CUSTOM_DOMAIN" ]; then
echo "$CUSTOM_DOMAIN" > "$SITE_DIR/CNAME"
fi
# Git init + first commit
cd "$SITE_DIR"
git init -b main
git add .
git commit -m "chore: initial site scaffold"
# Create GitHub repo (skip with --in-repo)
if [ "$IN_REPO" = "false" ]; then
gh repo create "$REPO_NAME" --public --source=. --push
# Enable Pages
gh api -X POST "/repos/$GH_USER/$REPO_NAME/pages" \
-f "source[branch]=main" \
-f "source[path]=/" || true # 422 means already enabled
# Poll until built (5 min timeout)
DEADLINE=$(($(date +%s) + 300))
STATUS="null"
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
STATUS=$(gh api "/repos/$GH_USER/$REPO_NAME/pages" -q .status 2>/dev/null || echo "null")
if [ "$STATUS" = "built" ]; then break; fi
sleep 10
done
if [ "$STATUS" != "built" ]; then
echo "⚠ Pages build did not complete within 5min — check https://github.com/$GH_USER/$REPO_NAME/settings/pages"
SKIP_URL_WRITEBACK=true
fi
fi
7. Write URLs back to app-info.json (init mode only)
if mode == "init" and not skip_url_writeback and not in_repo:
app_info_path = "appstore/app-info.json"
info = json.load(open(app_info_path))
new_marketing = site_url + "/"
new_support = site_url + "/support.html"
# Confirm on conflict
if info.get("marketing_url") and info["marketing_url"] != new_marketing:
print(f"marketing_url exists: {info['marketing_url']}")
print(f"would overwrite with: {new_marketing}")
confirm = input("Overwrite? [y/N] ").strip().lower()
if confirm != "y":
return
info["marketing_url"] = new_marketing
info["support_url"] = new_support
with open(app_info_path, "w") as f:
json.dump(info, f, indent=2)
8. update mode
Run steps 1-4 with existing site dir. Then:
cd "$SITE_DIR"
git pull --rebase origin main || true
# Write rendered files
# Commit only if changed
if git diff --quiet && git diff --cached --quiet; then
echo "No changes."
exit 0
fi
git add .
git commit -m "chore(site): regenerate from brand book"
git push
9. plan mode
Run steps 1-4. Don't write anything. Print a diff-style summary of what would change:
- Which files would be created/modified in the site dir
- Whether
app-info.jsonwould be updated - Whether the repo would be created
10. Report
ios-site — init complete
────────────────────────
Repo: https://github.com/{gh_user}/{repo_name}
Pages: {site_url}/
Support: {site_url}/support.html
Written to appstore/app-info.json:
marketing_url = {site_url}/
support_url = {site_url}/support.html
Next:
/ios-store-listing — generate listing.json using the new URLs
/appstore-iac plan — preview the full App Store sync
Safety
- Default is plan mode (unless explicit
initorupdate). - Never force-push. Repo name conflicts stop and ask.
- Validate templates after render — no leftover
{{...}}tokens. --in-repotarget must be empty if creating fresh.- No secrets in site.config.json — documented in reference.md.
- Self-healing. On any gh API failure, fix and update
reference.mdin the same run.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Jakeintech
- Source: Jakeintech/claude-ios-skills
- 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.