Install
$ agentstack add skill-krusemediallc-uni1-image-ad-uni1-image-ad ✓ 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.
About
uni1-image-ad
Generate one or more uni-1 images and create paused Meta ads from them, attached to an existing ad set in the user's Meta ad account.
Hard rules — never relax
These come from the user's Luma brand contract and from "don't spend money by accident":
- Model is
uni-1. Never substitutephoton-1,photon-flash-1, or any other model. The brand contract requires every promotional image be generated by uni-1. The helper script enforces this; you must not work around it. - Always clone from an existing ad. The user must supply an existing ad ID to clone — page, ad set, link URL, and CTA are read from that ad. Never hand-pick a page or ad set independently. If the user does not provide an ad ID, ask for one before any other input.
- Claude writes the new ad copy. Body and headline are generated fresh per variant, tailored to the new image. The cloned ad's copy is reference for tone/voice only — never copy it verbatim.
- Ad status is
PAUSED. Never pass--status ACTIVEor otherwise unpause an ad. The user reviews and launches manually in Ads Manager. - No campaign or ad-set mutations. New ads are attached to the cloned ad's existing ad set. The skill never creates campaigns or ad sets.
- Confirmation gate before every Meta CLI mutation. Show the full command, wait for explicit approval, then run.
- Audit log first, then mutate. Append the JSONL row when a creative is created, before attempting the ad. Orphaned creative IDs must be recoverable from
./generated/runs.jsonl.
- No screenshot/platform chrome in generated images. The output of
generate_image.pymust be the standalone ad creative — the static image the advertiser uploads. Never render iOS device chrome (status bar, home indicator), platform brand-row headers ("Sponsored"/"Saved"), post text/captions surrounding the ad, link-card footers ("URL.COM | headline | Learn more"), engagement rows (likes/comments/shares), or platform tab bars. The script auto-appends a no-chrome guard suffix to every prompt by default. If a use case genuinely requires screenshot-style chrome (rare — usually a UGC-aesthetic ad), pass--allow-chromeexplicitly and explain why.
Generation modes
The helper script supports two modes; pick based on what the user asks for.
| Mode | When to use | Required | Optional | |---|---|---|---| | image (default) | Generate a brand-new ad image. | --prompt, --aspect-ratio | --image-ref (up to 8) for product/style grounding | | image_edit | Modify an existing image (swap colors, change background, add an element). | --prompt, --source | --image-ref (up to 8) for additional guidance |
Reference images are how you keep brand fidelity. When the user wants the ad to feature a specific product (jar, bottle, package), pass that product image via --image-ref. The model holds packaging, wordmarks, and label typography much more faithfully when it has the asset to ground on. Without a reference, output is "inspired by" — labels and wordmarks will be approximate.
Refs and source can be local file paths (PNG/JPG/JPEG/WEBP/GIF). The script base64-encodes them; no need to upload anywhere.
Inputs the user must provide (or you must elicit)
The user-facing surface is small — most ad fields come from the cloned ad. Cache last_clone_ad_id per account in ~/.claude/skills/uni1-image-ad/state.json.
| Input | Source | Notes | |---|---|---| | Existing ad ID to clone | User (required) | Ad must live in the configured AD_ACCOUNT_ID. Cache as default for next run. | | Seed prompt | User | The creative direction in their words. You will rewrite it (see Phase 3). | | Mode | User or inferred | image (default) or image_edit. Use image_edit only if the user is editing a specific source image. | | Source image (--source) | User | Required only for image_edit. | | Reference image(s) (--image-ref) | User | Optional but strongly recommended when the ad features a specific product. Up to 8. | | Variant count N | User, default 1 | Cap at 5. | | Aspect ratio | User | One of 1:1, 1.91:1, 4:5, 9:16. Reject anything else. Ignored in image_edit mode. | | Ad name (base) | User, optional | Defaults to · uni-1 vN. You suffix per variant. | | CTA override | User, optional | Defaults to the cloned ad's CTA. Override only if user explicitly asks. |
Derived from the cloned ad (do NOT ask the user — read these via meta ads ad get + meta ads creative get):
| Field | Comes from | |---|---| | Ad set ID | ad.adset_id of the cloned ad | | Page ID | creative.object_story_spec.page_id of the cloned creative | | Link URL | creative.object_story_spec.link_data.link | | CTA | creative.object_story_spec.link_data.call_to_action.type | | Cloned body / title | Reference for tone only — used in Phase 3.5 to write new copy |
Workflow
Phase 1: Preflight
Verify in this exact order; bail on the first failure with a fix-it message.
- The current working directory has a
.envfile containingLUMA_API_KEY,ACCESS_TOKEN, andAD_ACCOUNT_ID. - Run
meta auth status. It must printAuthenticated. On non-zero, instruct the user to refresh their token (seereferences/meta-cli-flags.md§ Token refresh quick steps). - Read
~/.claude/skills/uni1-image-ad/state.json. Ifaccounts[].last_clone_ad_idexists, offer it as the default for "ad ID to clone".
Phase 2: Gather inputs and clone the source ad
- Ask for the ad ID to clone if not already supplied. Show cached
last_clone_ad_idas the default. Refuse to proceed without one.
- Resolve the ad via
meta -o json ads ad get. From the response, capture:
adset_id— the destination for the new adcreative.id— to fetch the cloned creative
- Resolve the creative via
meta -o json ads creative get. From the response, capture:
object_story_spec.page_id(orobject_story_spec.link_data.page_id) → page_idobject_story_spec.link_data.link→ link_urlobject_story_spec.link_data.call_to_action.type→ CTAobject_story_spec.link_data.message→ cloned body (tone reference)object_story_spec.link_data.name→ cloned title (tone reference)
- Show the cloned values to the user as a compact summary so they can sanity-check. Example:
`` Cloning from ad 120246xxxxxxxxxxxx ("Old Headline · v3") Ad set: 120243594343360512 (Luma AI Testimonial Ads) Page: 1234567890 (Mr. Paid Social) Link: https://lumalabs.ai/api CTA: LEARN_MORE Body (cloned, for tone reference): "…" ``
- Then gather the new-image inputs: seed prompt, mode (default
image),--sourceif image_edit,--image-refpaths, variant countN, aspect ratio, optional ad-name base, optional CTA override.
- Validate aspect ratio strictly. Reject anything not in
{"1:1", "1.91:1", "4:5", "9:16"}.
Phase 3: Prompt rewrite
This is where you add value. First check the prompt library, then either fill a template or write fresh.
3a — Check the prompt library
Load references/prompt-library.md. It has 7 validated parameterizable templates:
| Tag | Format | When | |---|---|---| | T1 | Apple Notes listicle | "Why I switched to X" / sentimental list of reasons | | T2 | Editorial article hero | Publication-co-signed credibility ad (FORBES, WIRED, Vogue, etc.) | | T3 | Story tweet+UGC composite | Authority quote + UGC photo for Stories/Reels | | T4 | Fake Google search mosaic | "Best X for Y" with publication logos as social proof | | T5 | Comparison table (light) | Brand vs. category competitor with feature checklist | | T6 | Comparison table (dark, hooky) | Stop-the-scroll dark-mode "this RUINS X" hook | | T7 | Sticky-note + product flatlay | Tactile UGC-style, 30-day-test reviewer voice |
If the user's seed prompt or brief maps onto one of these, use it: read the matching section in prompt-library.md, fill in the {placeholder} variables for the user's brand, and use that as the rewritten prompt. Tell the user which template you matched and why.
If nothing fits, write fresh — then propose adding the new pattern to the library after generation succeeds.
3b — Fleshing out a fresh prompt (or filling a template)
When writing or completing a prompt, anchor on:
- Subject and pose
- Lighting and time of day
- Lens / framing
- Color palette / mood (pull from the brand's identity)
- Composition (rule of thirds, leading lines)
- Negative space for text overlay if the ad has body/headline copy
- Reference roles — if
--image-refis being used, name each reference's role explicitly in the prompt (e.g. "the product in imageref[0]", "the lighting from imageref[1]"). Luma's docs note that multi-reference quality improves when each reference is labeled in the prompt. - Standalone-creative scope — never describe iOS chrome, Sponsored badges, engagement counts, or platform UI. The script's no-chrome guard catches violations, but write the prompt as if the rule is on you.
Show the rewritten prompt to the user as one block. Tell them which template (if any) it's based on. Ask: "Use this, edit it, or start over?" Loop until approved.
Phase 4: Generate
Run the helper script. Always pass --env-file pointing at the project .env. Add --image-ref once per reference (max 8). Switch to --mode image_edit --source if the user is editing rather than creating from scratch.
# image mode (default), with optional brand reference
~/.claude/skills/uni1-image-ad/scripts/generate_image.py \
--prompt "" \
--aspect-ratio \
--n \
--image-ref \
[--image-ref ] \
--out ./generated \
--env-file .env
# image_edit mode (modify an existing image)
~/.claude/skills/uni1-image-ad/scripts/generate_image.py \
--prompt "" \
--mode image_edit \
--source \
[--image-ref ] \
--n \
--out ./generated \
--env-file .env
Reference-grounded generations typically take ~40–60s (vs ~6s text-only). Don't bail early — the script's poll timeout is 240s.
Each line on stdout is JSON for one variant (variant, path, generation_id, width, height). Display the paths to the user.
If any variant comes back below 1080×1080, regenerate it (the script logs a warning to stderr but still emits the JSON line — you decide whether to keep it). If all variants failed, stop and report the error from stderr.
Phase 5: Confirm variants
Show all paths and ask: "Use all / use these specific ones / regenerate / cancel." One confirmation covers all selected variants.
Phase 5.5: Write new ad copy
Two sub-steps: first read the data, then write copy informed by it. Don't skip the data read — generic copy is the #1 failure mode of this skill.
5.5a — Pull what's actually winning spend in this account
~/.claude/skills/uni1-image-ad/scripts/top_spending_ads.py \
--limit 10 --days 30
That prints one JSON-per-line for the top-10 spending ads in the configured account, with body, title, spend, impressions, ctr, and creative_id. Read all of it, then derive a short pattern profile:
- Voice — first-person? founder? testimonial? brand-distant?
- Length distribution — what's the median body length of the top performers?
- Bullet style —
→,✓,•, no bullets, emoji? - Hook structures — counter-narrative, statistic, personal anecdote, threat?
- Credibility anchors — recurring numbers, names, claims?
- CTA closers —
Join us →,See the demo,Comment X, no closer?
State the pattern profile to the user as a compact table before drafting copy. This is the diff between "I made up some copy" and "I matched what's working."
5.5b — Write copy that matches the pattern profile
For each selected variant, you (Claude) write a new body and headline. Anchor on:
- The new image — what it actually shows. Lead with the visual hook.
- The pattern profile from 5.5a — match length distribution, voice, bullet style, CTA closer. The data is the spec.
- The cloned ad's voice — secondary signal; never copy phrasing verbatim.
- A copy framework from
references/ad-copy-frameworks.md— load that file before writing. Pick a framework that fits the image + the dominant pattern in 5.5a. - For multi-variant runs, use different frameworks per variant so the user A/B tests structure, not just wording.
No hard character cap. Meta truncates around the read-more fold, but if the top spenders run 400-700 char bodies and that's clearly working in this account, write to that length. The data decides, not a rule of thumb. (Exception: titles still fit under ~40 chars to avoid placement-level truncation.)
Show all variants' copy as a numbered table for review, including which framework each used and a one-line note on which top-spending ad it pattern-matches:
v1 [framework: First Person] matches: 120245165245740512 ("Refresh. Duplicate. Pause. Pray.")
title: …
body: …
v2 [framework: Statistic] matches: 120243595999050512 ("The math doesn't lie.")
title: …
body: …
Ask: "Use this copy / edit specific lines / regenerate." Loop until approved before any Meta CLI calls.
Phase 6: Upload loop
For each selected variant i:
- Compose the creative-create command using the cloned
page_id,link_url, andCTA(or override) and the new copy from Phase 5.5. Usereferences/meta-cli-flags.mdfor the exact flag spelling. Quote all string values with embedded spaces.
``bash meta -o json ads creative create \ --name " v" \ --image \ --page-id \ --body "" \ --title "" \ --link-url "" \ --call-to-action ``
- Show the command, ask "run this?". Run on yes. Capture the creative ID.
- Append to
./generated/runs.jsonlimmediately (one JSON per line):
``json {"ts": "", "prompt": "", "variant": , "generation_id": "", "image_path": "", "creative_id": "", "creative_id_status": "created", "ad_id": null, "ad_id_status": "pending"} ``
- Compose the ad-create command using the cloned
adset_id:
``bash meta -o json ads ad create \ --name " v" \ --creative-id ``
- Show, confirm, run. Capture the ad ID. Update the JSONL row in place (read all lines, replace this one, rewrite) with
ad_idandad_id_status: "created".
- If ad-create fails: leave
ad_id_status: "failed"with anerrorfield. Tell the user the creative was created (give the ID) and offer to retry just the ad-create step.
Phase 7: Persist cache, report
- Update
~/.claude/skills/uni1-image-ad/state.json:
``json { "accounts": { "": { "last_clone_ad_id": "", "last_run_ts": "" } } } ``
- For each created ad, print:
- The ad ID
- Deep link:
https://business.facebook.com/adsmanager/manage/ads?act=&selected_ad_ids=(strip theact_prefix fromAD_ACCOUNT_ID)
- Remind the user: all ads are PAUSED — review and launch manually.
Retry mode (partial-failure recovery)
If runs.jsonl already exists when the user re-runs the skill, scan it for rows where creative_id_status == "created" but ad_id_status != "created". Offer:
> "I see orphan creative(s) from a previous run. Do you want to retry the ad create step against those existing creatives, or generate a new image?"
If retry: skip Phases 3–5, jump to Phase 6 step 4 with the existing creative IDs.
Dry-run mode
If the user says "dry run", "preview only", or "show me the commands", run Phases 1–4, then in Phase 6 just print every meta command without executing, and exit. Do not write to runs.jsonl in dry-run mode.
Out of scope — fail clearly
If the user asks for any of these, stop and explain it's not supported:
- Creating ads without cloning from an existing one. The skill always clones. If the user ha
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: krusemediallc
- Source: krusemediallc/uni1-image-ad
- 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.