| name | youtube-shorts-publish |
| description | Inspect, plan, authenticate, upload, schedule, and verify YouTube Shorts through the official YouTube Data API, for the user's own channel. Use whenever someone asks to upload, publish, post, or schedule a YouTube Short; check whether a video qualifies as a Short; safely dry-run a YouTube upload; configure YouTube upload OAuth; diagnose a Short stuck processing, rejected, or unexpectedly private; or verify an upload by video ID. Handles local ffprobe media checks, Desktop OAuth, private-by-default resumable uploads, Made for Kids and synthetic-media declarations, and post-upload processing checks. Not for generating or editing the video, bulk multi-channel posting, evading platform enforcement, or claiming Shorts Feed placement that the API cannot prove.
|
Publish YouTube Shorts safely
Use the bundled zero-dependency Node CLI. It separates local eligibility, API acceptance, video
processing, visibility, and Shorts classification instead of treating them as one success state.
Non-negotiable safety rules
- Work with one channel owned or controlled by the user. Never rotate accounts, proxies, or OAuth
clients to evade quotas or enforcement.
- Run a dry plan before every upload.
plan never reads OAuth files, contacts Google, or writes
local state. publish without --yes has the same guarantee.
- Only add
--yes and the reviewed --expected-plan-id when the user's request clearly authorizes
that exact upload and the plan's channel, source SHA-256, title, visibility, schedule, and
declarations match their intent.
- Keep the default
private unless the user explicitly asks for public, unlisted, or a future
publication time. Scheduling a private video means it will become public later.
- Never infer whether content is Made for Kids or contains realistic altered/synthetic media.
Obtain explicit
yes or no choices for both.
- Use the channel ID returned by
auth; the CLI refuses a write if the authorized channel differs.
- Treat a
plan_id as one-shot. Before creating an upload session, the CLI stores a receipt and a
read-only snapshot matching the reviewed SHA-256. A previous receipt blocks the same plan by
default, including after crashes and ambiguous responses.
- After an upload returns an ID, check that ID. Never search by title, delete a same-title video,
or blindly upload again because processing or verification timed out.
- Call an eligible upload a Shorts candidate. The Data API has no Shorts classification field,
so do not claim confirmed Shorts Feed placement.
Locate the CLI
Commands below assume the current directory is this skill directory. Otherwise use the absolute
path to scripts/ytshorts.mjs. Pass an absolute path for the source video when changing directories.
node scripts/ytshorts.mjs --help
node scripts/ytshorts.mjs doctor
Requirements are Node.js 20+ and ffprobe. There are no npm runtime dependencies.
Verify the installed contract without YouTube
Run node examples/verify-zero-publish.mjs with no arguments. It generates and removes a local
one-second portrait fixture, then exercises only inspect and plan. It does not run auth,
status, publish, or --yes; it points OAuth paths at absent sentinels and runs both CLI commands
under a network guard that first passes a deliberately blocked canary. Return its compact stdout
unchanged when the user asks for the evaluation result.
Treat passed: true only as a deterministic installation and safety-contract check. It does not
prove that a real upload will succeed or that YouTube will place media in the Shorts Feed. The
verifier requires ffmpeg as well as ffprobe; both are provided by FFmpeg.
After an independent installed-copy run, use the structured zero-publish tester form.
Choose completed only if the reporter independently installed and actually ran
youtube-shorts-publish for their own non-internal purpose and is not a maintainer, a maintainer's
teammate, or an internal tester. Use the incomplete outcome when the exact no-argument verifier did
not finish, and never include OAuth data, tokens, client secrets, authorization codes, resumable
URLs, a real channel ID, or personal paths. This is installation and safety-contract evidence, not
proof of a live upload or Shorts Feed placement.
First-time authorization
Read OAuth setup, then run:
node scripts/ytshorts.mjs auth
The system browser handles Google consent through a Desktop-app loopback flow. It requests
youtube.upload for insertion and youtube.readonly for the channel guard and owner-only status
read-back. Record the returned channel_id; pass it to every plan and publish command. Never open
or print the token file. If authorization is not needed for the current task, do not run auth
speculatively.
Inspect local media
node scripts/ytshorts.mjs inspect /absolute/path/short.mp4
Treat shorts_candidate: true as a local check against the published duration and displayed aspect
rules. Rotation and sample aspect ratio are applied before deciding whether the video is square or
vertical. A missing audio track is only a warning. Landscape media, unknown duration, or duration
over 180 seconds blocks publishing.
If the video needs conversion, let the user choose crop versus padding and then consult
media preparation. Do not silently alter their composition.
Build an exact, side-effect-free plan
node scripts/ytshorts.mjs plan /absolute/path/short.mp4 \
--title "Launch day in 30 seconds" \
--description-file /absolute/path/description.txt \
--channel-id UCxxxxxxxxxxxxxxxxxxxxxx \
--privacy private \
--made-for-kids no \
--contains-synthetic-media no
Useful metadata flags:
--tag value is repeatable; comma-separated values also work.
--category-id 22 defaults to People & Blogs.
--notify-subscribers yes|no defaults to no.
--default-language en sets the metadata language.
--publish-at 2026-08-10T09:00:00+08:00 requires --privacy private and represents future
public visibility. It must remain at least 30 minutes away throughout session creation and chunk
transfer; otherwise the CLI stops rather than risk a past timestamp becoming public immediately.
Inspect safe_to_publish, blocking_issues, warnings, target, source sha256, plan_id, and
the exact API request (including notifySubscribers). Resolve every blocker before proceeding.
The channel is verified online only after confirmation; the plan marks that explicitly.
Publish only after reviewing the plan
The same command without --yes is still a dry run:
node scripts/ytshorts.mjs publish /absolute/path/short.mp4 \
--title "Launch day in 30 seconds" \
--channel-id UCxxxxxxxxxxxxxxxxxxxxxx \
--privacy private \
--made-for-kids no \
--contains-synthetic-media no
For a user-authorized real upload, repeat the unchanged command with the dry run's plan_id and
--yes:
node scripts/ytshorts.mjs publish /absolute/path/short.mp4 \
--title "Launch day in 30 seconds" \
--channel-id UCxxxxxxxxxxxxxxxxxxxxxx \
--privacy private \
--made-for-kids no \
--contains-synthetic-media no \
--expected-plan-id PLAN_ID_FROM_DRY_RUN \
--yes
The CLI hashes the video and canonical request again before touching OAuth. A changed video,
description file, metadata flag, or target produces PLAN_CHANGED and no upload starts. After the
channel check it copies those reviewed bytes to a 0400 local snapshot and atomically creates an
operation receipt immediately before mutation.
The uploader creates one resumable session, sends fixed 8 MiB chunks, follows YouTube's 308 Range
offset after interruptions, renews OAuth access before expiry without restarting the session, and
polls the returned video ID for up to 60 seconds by default. Set --wait-seconds 0..900 to change
that window. Progress goes to stderr; structured results go to stdout.
One plan_id gets one upload attempt by default. Receipts live under the configured
operations/ directory and contain no OAuth token or resumable session URL. If a prior attempt is
ambiguous, inspect its receipt and YouTube Studio. Only after confirming no video exists may the
user explicitly authorize the same command with --new-attempt; never add that flag automatically.
The CLI permits at most one such exceptional retry and refuses it when any prior receipt already
contains a video ID. It also requires the primary receipt itself to say ambiguous; a preparing or
active-looking receipt is fail-closed so a second process cannot upload concurrently.
Interpret and recheck the result
| state | meaning | next action |
|---|
uploaded_processing | bytes arrived; YouTube is still processing | wait, then run status; do not re-upload |
processed_private | processing succeeded; actual visibility is private | review metadata or publish manually when intended |
processed_unlisted / processed_public | processing and requested visibility succeeded | report success, still as a Shorts candidate |
processed_scheduled | processing succeeded and future public release remains scheduled | report the schedule and preserve private-until-publish state |
processing_failed / rejected | YouTube failed or rejected the video | report the returned reason; fix before a new upload |
verification_timeout | a video ID exists, but the wait window ended | run status VIDEO_ID; do not re-upload |
not_found_after_insert | ID returned but read-back is not visible yet | preserve the ID and recheck; treat outcome as unresolved |
node scripts/ytshorts.mjs status VIDEO_ID
node scripts/ytshorts.mjs status VIDEO_ID --wait-seconds 120
If public or unlisted was requested but read-back says private, report privacy_restricted and
explain that an unaudited API project is a common cause. Do not auto-update visibility. Read
YouTube API and Shorts behavior before diagnosing policy,
quota, Content ID, scheduling, or classification behavior.
For publish, top-level upload_accepted: true means YouTube returned a video ID. Top-level
ok: true requires semantic verification too. A false ok with a video ID is not permission to
retry; follow verification.state and the receipt.
Troubleshooting
| symptom | likely cause | response |
|---|
FFPROBE_NOT_FOUND | ffprobe is not installed or not on PATH | install FFmpeg or set YTSHORTS_FFPROBE_BIN |
CLIENT_SECRETS_NOT_FOUND | Desktop OAuth JSON is not configured | follow references/oauth-setup.md |
OAUTH_REAUTH_REQUIRED | token revoked/expired; Testing projects often expire it after 7 days | run auth again; review consent-screen publishing status |
OAUTH_REFRESH_TOKEN_REQUIRED | stored authorization cannot renew during a long upload | run auth again; do not substitute a short-lived access token |
CHANNEL_MISMATCH | Google authorized a different channel | stop; authorize/select the intended channel or correct --channel-id |
PLAN_CHANGED | source bytes or normalized request differ from the reviewed dry run | inspect the new plan; never reuse the old ID |
PLAN_ALREADY_ATTEMPTED | this exact plan already has a durable receipt | inspect the receipt and Studio; default is never retry |
PLAN_VIDEO_ALREADY_EXISTS | a prior receipt already records a YouTube video ID | check that video; this plan is permanently ineligible for retry |
PRIOR_ATTEMPT_NOT_AMBIGUOUS | the prior receipt is preparing, active-looking, or completed | do not override it; this prevents concurrent duplicate uploads |
UPLOAD_OUTCOME_AMBIGUOUS | a session started but completion could not be proven | do not repeat; inspect Studio, then require explicit user approval for --new-attempt if absent |
| requested public, actual private | API project may require a YouTube compliance audit | keep the video private and follow Google's audit path |
| state remains processing | normal asynchronous transcode or an eventual processing issue | re-run status; never duplicate the upload |
Do not expose OAuth codes, bearer/refresh tokens, client secrets, or resumable session URLs in chat,
logs, commits, or issue reports.