Write a TikTok caption under 2,200 chars (hashtags included), pick a tasteful 3 to 5 hashtag set with mixed reach, and set the platformSettings.tiktok flags (viewer setting, comments, duet, stitch, commercial and branded content). Front-loads a reason to comment, runs the humanizer pass, and publishes the rendered video via Publora on approval. Use to caption a finished video. Not for the in-video hook (use tt-hook-scripter) or scrubbing a script (use tt-humanizer).
npx skills add https://github.com/sergebulaev/tiktok-skills --skill tt-caption-writer
Write the caption that sits under the video: a tight caption inside the 2,200
char API limit, a mixed-reach hashtag set, and the platformSettings.tiktok
flags that control reach and interaction. On TikTok the caption supports the
video, it does not carry it. The hook is in the first 1-3 seconds of the clip
(see tt-hook-scripter); the caption's job is to add context and earn a comment.
loaded with a reason to read or comment
is supplied
Build these with lib.tiktok_settings(...). Defaults match Publora's.
| Setting | Values / default | What it does |
|---|---|---|
| viewerSetting | PUBLIC_TO_EVERYONE (default), MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY | who can view. Effectively required; an empty value is rejected |
| allowComments | true (default) | viewers can comment |
| allowDuet | false (default) | viewers can Duet |
| allowStitch | false (default) | viewers can Stitch |
| commercialContent | false (default) | the video is commercial |
| brandOrganic | false (default) | promoting your own brand |
| brandedContent | false (default) | paid partnership / sponsored |
allowComments,allowDuet, allowStitch to TikTok's disable_* flags, so the booleans can
land inverted (sending allowDuet: true may disable duets). The status cannot
be confirmed from the API alone. **Test with a SELF_ONLY draft before
trusting these values.** Tell the user this whenever they change a flag.
commercialContent is true, at least one ofbrandOrganic (your own brand) or brandedContent (paid partnership) must
also be true, or Publora rejects it. lib.tiktok_settings enforces this.
review, posts are forced to SELF_ONLY regardless of viewerSetting. Warn the
user if a "public" post lands private.
PUBLIC_TO_EVERYONE. For an end-to-end test, use SELF_ONLY.Voice profile first (all drafts). If ../../references/voice-profile.md has filled: yes, load it and match the user's voice fingerprint, hard rules, and CTA/link style throughout. If it is not filled, mention once that tt-humanizer --mode profile can learn their voice from a few posts, then proceed with the generic voice rules.
Never invent the specifics. The rules below ask for a concrete number, a date and a named entity, because that is what separates a real post from a generated one. Take them from what the user actually said in this conversation. Do not invent a figure, a date, a client name or a result, and do not soften a vague claim into a plausible-looking number. If the user has nothing concrete for a beat, ask them once, and if they still have nothing, drop the claim rather than decorate it. A published invented number is a retraction; a missing one is only a weaker post.
then shape for it: comments -> end on one specific question tied to the video;
saves -> promise reference value ("the 3 settings are in the caption");
shares -> a claim people will want to show someone; profile taps -> an open
loop the pinned video pays off. Then gather the video topic/script, the
niche, whether it is commercial, and whether a rendered
.mp4 path exists.
or a specific question to comment on. Keep it tight. Em dashes under the cap
(about one per 100 words; a short caption rarely needs one), no AI vocab
cluster. Optionally restate the hook's open loop as a comment prompt.
one to two specific to the video. Put them at the end. Never stuff.
PUBLIC_TO_EVERYONE, comments on, duet/stitchoff, no commercial flags. Adjust per the user, and surface the two gotchas.
Build with lib.tiktok_settings(...).
one per 100 words, never swap one for a period), break stacked triads,
generic openers, reveal bridges and sincerity openers ("not gonna lie").
Leave the rhythm alone; never chop a line for punch. Canonical rules:
tt-humanizer V3.
and the resolved viewerSetting (flag if it may post private).
profile URL>, video_path=<path or None>, platform_settings=<settings>,
scheduled_time=<iso or None>, platforms=[<TIKTOK_PLATFORM_ID>])`. With a video
path and Publora configured, it runs draft -> upload -> schedule. Without one,
it returns the caption and settings to upload in-app.
Global voice rules: see root SKILL.md Voice rules. Additional skill-specific
rules:
viewerSetting. Never ship an empty value.commercialContent: true with neither brand flag set.../../references/voice-rules.md - caption and hashtag rules../../references/algorithm-heuristics.md - caption, hashtag, and settings heuristicsreferences/settings-matrix.md - every platformSettings.tiktok value, defaults, and the known bugslib/publora_client.py - tiktok_settings(...), publish_video(...), create_draft(...)Offer a generated image when a visual would lift reach. Draft a prompt and call
lib.illustrate(prompt, kind="story"), pulling brand handle/color from Voice &
Brand Profile section 6 for a pixel-exact overlay. Show the returned url + cost,
for a TikTok photo post, attach the image via media_urls=[url] (TikTok photo carousel); for a video, TikTok's API has no cover-image field, so set the cover in the TikTok app. Full workflow (incl. quote-cards):
../tt-humanizer/sub-skills/illustration.md. No Pixfaro key -> it drafts the prompt for you to generate manually.
tt-hook-scripter - the in-video hook (the caption does not replace it)tt-humanizer - scrub the caption before publishingtt-content-planner - schedule captions across a posting weekTake sergebulaev/tt-caption-writer from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.