YouTube Skill is an extensible plugin for safe, natural-language YouTube workflows in Codex and Claude Code. Platform-specific skill adapters stay separate while sharing one deterministic Python engine for YouTube Data API operations.
The first bundled skill is YouTube Upload (youtube-upload). It prepares and validates upload metadata, handles desktop OAuth, performs resumable uploads, and can optionally set a thumbnail or add the uploaded video to a playlist.
Important
Uploading is an external side effect. The plugin defaults to a dry run, private visibility, and subscriber notifications disabled. It never uploads unless execution is explicitly requested.
- Natural-language intake for upload metadata
- YAML or JSON manifests with deterministic validation
- OAuth 2.0 desktop authorization with refreshable local tokens
- Resumable video uploads with bounded retry behavior
- Private, unlisted, public, and scheduled publication
- Explicit made-for-kids and realistic synthetic-media disclosure
- Optional custom thumbnail and playlist insertion
- Preserved video IDs and local result logs after partial failures
- Separate Codex and Claude Code adapters over one shared engine
The upload workflow intentionally requires progressive confirmation:
| Action | Required CLI flags | Behavior |
|---|---|---|
| Validate or dry run | none | Normalizes metadata and reports warnings without contacting YouTube |
| Upload privately | --execute |
Uploads with privacy_status: private |
| Upload unlisted or public | --execute --confirm-public |
Requires a second explicit visibility confirmation |
| Schedule publication | --execute --confirm-public |
Uploads privately with a future publish_at value |
Additional safeguards:
made_for_kidsandcontains_synthetic_mediamust always be set explicitly.- Public, unlisted, and scheduled manifests emit warnings during validation.
- Subscriber notifications default to
falseeven though the YouTube API default istrue. - OAuth secrets and tokens are kept outside the repository.
- An uploaded video is never deleted automatically if thumbnail or playlist processing fails.
Codex: $youtube-upload ───────┐
├── shared manifest validator and YouTube API engine
Claude: /youtube-skill:youtube-upload ─┘
The adapters turn a natural-language request into the same validated manifest. The shared engine then performs dry runs, authorization, uploads, and optional post-upload operations.
- Python 3.11 (the version used by CI)
- A Google account with a YouTube channel
- A Google Cloud project with YouTube Data API v3 enabled
- A Desktop app OAuth client
- Codex or Claude Code for natural-language skill invocation
Add this repository as a marketplace and install the plugin:
codex plugin marketplace add junjunjunbong/youtube-skill
codex plugin add youtube-skill@junwon-youtube-toolsStart a new Codex task after installation so the new skill is loaded. Invoke it explicitly:
$youtube-upload Prepare a private upload for /absolute/path/to/video.mp4.
In Claude Code, add the marketplace and install the plugin:
/plugin marketplace add junjunjunbong/youtube-skill
/plugin install youtube-skill@junwon-youtube-tools
Invoke the skill explicitly:
/youtube-skill:youtube-upload Prepare a private upload for /absolute/path/to/video.mp4.
For local Claude Code development, clone the repository and load it directly:
git clone https://github.com/junjunjunbong/youtube-skill.git
cd youtube-skill
claude --plugin-dir .Clone the repository and create an isolated Python environment:
git clone https://github.com/junjunjunbong/youtube-skill.git
cd youtube-skill
python3 -m venv common/youtube-upload/.venv
source common/youtube-upload/.venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install -r common/youtube-upload/requirements.txtAll commands below assume the repository root as the current directory and the virtual environment is active.
-
Create or select a project in Google Cloud Console.
-
Enable YouTube Data API v3.
-
Configure the OAuth consent screen.
-
Create an OAuth client ID with application type Desktop app.
-
Download the JSON file and store it outside the repository, for example:
~/.config/youtube-upload/client_secret.json -
Authorize once:
python3 common/youtube-upload/scripts/youtube_auth.py \ --client-secrets ~/.config/youtube-upload/client_secret.json \ --token-path ~/.config/youtube-upload/token.json
The browser authorization flow requests the youtube.force-ssl scope and writes a refreshable token with file mode 0600. See the detailed OAuth setup guide.
Default credential paths:
| Purpose | Default path |
|---|---|
| OAuth client secrets | ~/.config/youtube-upload/client_secret.json |
| Refreshable token | ~/.config/youtube-upload/token.json |
| Result logs | ~/.local/state/youtube-upload/ |
Environment overrides:
export YOUTUBE_CLIENT_SECRETS=/absolute/path/to/client_secret.json
export YOUTUBE_TOKEN_FILE=/absolute/path/to/token.json
export YOUTUBE_RESULT_LOG_DIR=/absolute/path/to/result-logsCommand-line --client-secrets and --token-path values take precedence over their corresponding environment variables.
The adapter extracts values already supplied in the request and asks only for required decisions that are still missing. Before an upload it must know:
- Local video path
- Video title
- Whether the video is made for kids
- Whether it contains realistic altered or synthetic media
Description, tags, category, thumbnail, and playlist are optional. Scheduling is requested only when you ask for it. The safe defaults are private visibility, no schedule, no thumbnail, no playlist, and no subscriber notification.
Example request:
$youtube-upload Upload /absolute/path/to/demo.mp4 privately with the title
"Project demo". It is not made for kids and contains no realistic synthetic media.
Do not notify subscribers. Add it to playlist PLxxxxxxxxxxxxxxxx.
Before execution, the skill presents one normalized summary covering the file, title, visibility or schedule, audience, synthetic-media disclosure, notifications, thumbnail, and playlist. Actual upload begins only after the required confirmation.
Copy the example beside your own working files:
cp common/youtube-upload/examples/upload.example.yaml /path/to/upload.yamlEdit /path/to/upload.yaml:
video_path: "./videos/example.mp4"
snippet:
title: "Example private upload"
description: |
A description for the uploaded video.
tags:
- "example"
- "demo"
category_id: "22"
status:
privacy_status: "private"
publish_at: null
made_for_kids: false
contains_synthetic_media: false
notify_subscribers: false
assets:
thumbnail_path: null
playlist_id: null
execution:
result_log_dir: nullRelative file paths are resolved from the directory containing the manifest, not from the shell's current directory.
python3 common/youtube-upload/scripts/validate_manifest.py \
--manifest /path/to/upload.yamlThe command prints normalized JSON and warnings. It does not authenticate or upload anything.
python3 common/youtube-upload/scripts/youtube_upload.py \
--manifest /path/to/upload.yamlWithout --execute, the uploader always returns a dry-run result.
After reviewing the normalized metadata:
python3 common/youtube-upload/scripts/youtube_upload.py \
--manifest /path/to/upload.yaml \
--executeSet privacy_status to public or unlisted for immediate publication, then provide both execution flags:
python3 common/youtube-upload/scripts/youtube_upload.py \
--manifest /path/to/upload.yaml \
--execute \
--confirm-publicFor scheduled publication, keep privacy_status: private and set publish_at to a future ISO 8601 timestamp with an explicit UTC offset, such as 2030-01-15T09:00:00+09:00. Scheduled execution also requires --confirm-public.
See the complete input schema and validation rules.
| Field | Required | Default | Notes |
|---|---|---|---|
video_path |
yes | none | Must resolve to an existing file |
snippet.title |
yes | none | Non-empty, at most 100 characters |
snippet.description |
no | "" |
At most 5,000 UTF-8 bytes |
snippet.tags |
no | [] |
Uses YouTube's 500-character accounting rule |
snippet.category_id |
no | null |
Numeric category ID represented as a string or number |
status.privacy_status |
no | private |
private, unlisted, or public |
status.publish_at |
no | null |
Future ISO 8601 timestamp with an offset; requires private |
status.made_for_kids |
yes | none | Must be explicitly true or false |
status.contains_synthetic_media |
yes | none | Disclosure for realistic altered or synthetic content |
status.notify_subscribers |
no | false |
Emits a validation warning when enabled |
assets.thumbnail_path |
no | null |
JPEG or PNG, no larger than 2 MB |
assets.playlist_id |
no | null |
Existing playlist ID, not a playlist name or URL |
execution.result_log_dir |
no | environment or local state directory | Overrides the result-log destination |
Titles and descriptions cannot contain < or >. A public, unlisted, scheduled, or notification-enabled manifest produces a warning so the final intent is visible before execution.
An execution result includes the video ID, YouTube URL, final privacy status, scheduled time, warnings, thumbnail status, playlist status, errors, and result-log path.
- Results are written as JSON under
execution.result_log_dir,YOUTUBE_RESULT_LOG_DIR, or~/.local/state/youtube-upload/, in that order. - Network failures and HTTP
500,502,503, and504responses are retried with bounded exponential backoff and jitter, up to eight retries. - Invalid metadata, quota, permission, and policy errors are not retried automatically.
- If the video upload succeeds but thumbnail or playlist processing fails, the command returns a partial failure while preserving the uploaded video ID and result log.
- The uploader never performs an automatic delete or rollback of a successfully uploaded video.
videos.insertaccepts uploads up to 256 GB withvideo/*orapplication/octet-streammedia types.- Uploads made through unverified API projects created after July 28, 2020 may be restricted to private visibility until the project passes a compliance audit.
status.publishAtcan be set only for a private video that has never been published. This project additionally requires a future timestamp with an explicit timezone offset.thumbnails.setaccepts files up to 2 MB. This project deliberately restricts thumbnail inputs to JPEG and PNG.- Playlist insertion uses
playlistItems.insertand requires access to the target playlist.
YouTube quotas, channel eligibility, and policy enforcement remain outside this plugin's control.
If the command reports missing Google API packages, activate the project virtual environment and install the pinned dependency ranges:
source common/youtube-upload/.venv/bin/activate
python3 -m pip install -r common/youtube-upload/requirements.txtConfirm that the Desktop app client JSON exists at the default location, pass --client-secrets, or set YOUTUBE_CLIENT_SECRETS. Do not paste the file contents into an issue or conversation.
Run validate_manifest.py and fix the reported field before attempting execution. Common causes include a missing video, omitted audience or synthetic-media booleans, an expired schedule, invalid visibility, oversized text, or an invalid thumbnail.
Public, unlisted, and scheduled uploads require both --execute and --confirm-public. The second flag is an intentional confirmation boundary, not an authentication option.
Check whether the Google Cloud API project is unverified. YouTube restricts uploads from certain unverified projects to private visibility until the project completes an audit.
The uploader does not retry these errors because repeating the same request will not resolve the underlying condition. Review the returned API error, channel permissions, project quota, and policy status before trying again.
Use the returned video ID and result-log path to continue manually. The original video remains uploaded; the plugin never deletes it as an automatic rollback.
.codex-plugin/plugin.json # Codex plugin manifest
.agents/plugins/marketplace.json # Codex marketplace metadata
.claude-plugin/ # Claude Code plugin and marketplace metadata
skills/youtube-upload/ # Codex skill adapter
claude/skills/youtube-upload/ # Claude Code skill adapter
common/youtube-upload/
├── examples/ # Example upload manifest
├── references/ # Input schema and OAuth guide
├── requirements.txt # Shared Python dependencies
└── scripts/ # Validator, OAuth, and uploader engine
tests/ # Manifest and safety tests
Install dependencies, run the tests, and compile all Python entry points:
python3 -m pip install -r common/youtube-upload/requirements.txt
python3 -m unittest discover -s tests -v
python3 -m py_compile common/youtube-upload/scripts/*.pyGitHub Actions runs the same checks on every push and pull request using Python 3.11.
- Never commit
client_secret.json,token.json, access tokens, refresh tokens, manifests containing private metadata, or result logs. - The included
.gitignoreexcludes common OAuth credential names, virtual environments,*.log, andupload-result*.jsonfiles. - Keep OAuth files outside the repository and restrict access to the local user.
- Review normalized metadata before every upload and use private visibility while testing.
- If a credential is exposed, revoke it in Google Cloud immediately and create a replacement.