yt-studio is a script-first command-line client for publishing and managing
YouTube videos without clicking through YouTube Studio. It supports operations
that are missing from the official YouTube Data API, including native title and
thumbnail experiments, Shorts related videos, info cards, and end screens.
The Python package and CLI command are currently named ytstudio-safe for
backward compatibility:
ytstudio-safe --help
# or
python -m ytstudio --helpWarning
This is an unofficial tool. It uses private YouTube Studio endpoints that can change without notice. Automated use may violate Google or YouTube terms or trigger account enforcement. Start with a disposable test channel, review every dry run, and keep captured sessions private.
- Upload long-form videos and Shorts with resumable media transfer.
- Set title, description, tags, audience, thumbnail, and visibility.
- Publish immediately or schedule a timezone-aware release.
- Run a native two- or three-arm title-and-thumbnail A/B test.
- Attach a related long-form video to a Short.
- Replace info cards and end screens from one JSON file.
- Publish an entire long-form-plus-Shorts workflow from an idempotent manifest.
- Save progress atomically so an interrupted workflow can resume safely.
Every publishing command is a dry run by default. A mutation requires both
--apply and an exact --confirm value. Subscriber notifications are disabled
by the client.
- Python 3.10 or newer
- Google Chrome for account sign-in and session capture
uvorpip
Send the agent this repository URL and the following instruction:
Install yt-studio and complete a read-only onboarding.
1. Detect my operating system and install missing Python 3.10+, Git, uv,
Playwright and a supported dedicated browser. Handle package-manager
commands yourself; stop only for an administrator password or OS approval.
2. Clone the repository and install ytstudio-safe with Playwright.
3. Open a new dedicated ytstudio-safe browser profile. I will only sign in to
Google/YouTube and select the intended channel.
4. Capture the filtered session, bind it to the exact channel ID and keep every
credential outside the repository with private file permissions.
5. Verify authentication and show a dry run. Do not upload, schedule, publish,
comment or change Studio without separate explicit permission.
The user should only need to complete Google sign-in, 2FA/CAPTCHA if requested, and choose the intended YouTube channel. The agent handles installation, session capture and read-only checks.
Install locally with uv:
git clone https://github.com/sstani-bgv/yt-studio.git
cd yt-studio
uv run --with 'playwright>=1.45' --with-editable . \
python -m playwright install chromiumOr install with pip:
python -m pip install -e '.[playwright]'
playwright install chromiumThe tool uses a dedicated Chrome profile. It does not read your normal Chrome, Arc, or ChatGPT browser profile. The captured session is a credential: never commit it, share it, or place it inside this repository.
ytstudio-safe auth-open \
--session-name youtube-sandbox \
--browser-channel chromeSign in, select the intended channel, wait for its Studio dashboard, and fully quit that Chrome window.
ytstudio-safe auth-capture \
--session-name youtube-sandbox \
--browser-channel chrome \
--expected-channel-id UC_REPLACE_WITH_CHANNEL_IDFor an explicitly acknowledged production session, add:
--session-role production --acknowledge-private-api-risk
Before every mutation, the client checks that the active Studio channel matches the captured channel ID. A mismatch fails before the request is sent.
ytstudio-safe security-capture --session-name youtube-sandboxThis briefly opens the dedicated browser, completes Studio's native security
flow, saves the token with mode 0600, and closes the browser. The token expires
after 90 minutes. The remaining work is sent directly over HTTP; the CLI does
not keep clicking through Studio.
Profiles are stored outside the repository under:
~/.local/share/ytstudio-safe/profiles/<session-name>
- Mutations are dry-run-first and require an exact target confirmation.
- Direct requests verify the active channel before each mutation.
- Session data is filtered, atomically stored, locked, and written as
0600. - Cookie values, authorization hashes, upload URLs, and API keys are redacted.
- Credential-bearing redirects are disabled and network hosts are allow-listed.
- No downloaded JavaScript is evaluated in Python.
- Public visibility requires the additional
--allow-publicflag. - Browser fallback is never automatic; it requires
--browser-fallback.
Review the dry run:
ytstudio-safe upload \
--file /absolute/path/video.mp4 \
--title 'Video title' \
--description-file /absolute/path/description.txt \
--tag ai --tag automation \
--thumbnail /absolute/path/thumbnail.png \
--privacy UNLISTEDApply it using the dedicated session. The confirmation must be
UPLOAD:<filename>:
ytstudio-safe upload \
--session-name youtube-sandbox \
--file /absolute/path/video.mp4 \
--title 'Video title' \
--description-file /absolute/path/description.txt \
--tag ai --tag automation \
--thumbnail /absolute/path/thumbnail.png \
--privacy UNLISTED \
--apply --confirm 'UPLOAD:video.mp4'Shorts use the same command; YouTube detects the format from the media. Uploads are transferred through Studio's resumable Scotty protocol and initially registered as private drafts before the requested visibility is applied.
Schedule during upload:
ytstudio-safe upload \
--session-name youtube-sandbox \
--file /absolute/path/short.mp4 \
--title 'Short title' \
--publish-at '2026-08-01T18:00:00+04:00' \
--apply --confirm 'UPLOAD:short.mp4'Schedule an existing video:
ytstudio-safe schedule \
--session-name youtube-sandbox \
--video-id VIDEO_ID \
--publish-at '2026-08-03T18:00:00+04:00'After reviewing the dry run, add --apply --confirm VIDEO_ID. A schedule must
be in the future and include a timezone offset.
Batch scheduling accepts this JSON:
{
"items": [
{"video_id": "VIDEO_ID", "publish_at": "2026-08-03T18:00:00+04:00"},
{"video_id": "SHORT_ID", "publish_at": "2026-08-05T18:00:00+04:00"}
]
}ytstudio-safe schedule-batch --input /absolute/path/schedule.json
ytstudio-safe schedule-batch \
--session-name youtube-sandbox \
--input /absolute/path/schedule.json \
--apply --confirm 'BATCH:schedule.json'Use update-visibility for an existing video. Setting PUBLIC also requires
--allow-public.
Provide two or three title/thumbnail pairs:
ytstudio-safe ab-test \
--session-name youtube-sandbox \
--video-id VIDEO_ID \
--title 'Variant A' --thumbnail /absolute/path/thumb-a.png \
--title 'Variant B' --thumbnail /absolute/path/thumb-b.png \
--title 'Variant C' --thumbnail /absolute/path/thumb-c.pngAfter reviewing the dry run, add --apply --confirm VIDEO_ID. Each thumbnail is
uploaded first; the experiment request references its encrypted Studio resource
ID instead of embedding image data in metadata.
ytstudio-safe related-video \
--session-name youtube-sandbox \
--short-id SHORT_ID \
--related-video-id LONG_FORM_VIDEO_IDAfter reviewing the dry run, add --apply --confirm SHORT_ID.
Both operations read one studio-features.json file:
{
"info_cards": {
"cards": [
{
"type": "video",
"target_video_id": "TARGET_VIDEO_ID",
"teaser_start_ms": 120000,
"custom_message": "Watch next",
"teaser_text": "Continue"
}
]
},
"end_screen": {
"start_ms": 503500,
"elements": [
{
"type": "video",
"video_type": "best_for_viewer",
"left": 0.0228,
"top": 0.5249,
"width": 0.3228,
"duration_ms": 20000
},
{
"type": "subscribe",
"channel_id": "UC_REPLACE_WITH_CHANNEL_ID",
"left": 0.8228,
"top": 0.5491,
"width": 0.1544,
"duration_ms": 20000
}
]
}
}Apply each validated section using the same file:
ytstudio-safe info-cards \
--session-name youtube-sandbox \
--video-id VIDEO_ID \
--input /absolute/path/studio-features.json
ytstudio-safe end-screen \
--session-name youtube-sandbox \
--video-id VIDEO_ID \
--input /absolute/path/studio-features.jsonAfter each dry run, add --apply --confirm VIDEO_ID. Supported cards are video,
playlist, and channel. Supported end-screen elements are video, playlist,
subscribe, and channel. Link cards are intentionally unsupported.
publish-project runs a complete, resumable workflow. A manifest can describe:
- one existing or newly uploaded long-form video;
- metadata and thumbnail experiment variants;
- the long-form video's info cards and end screen;
- multiple Shorts, their schedules, and their related-video link.
Create a plan without changing YouTube:
ytstudio-safe publish-project \
--manifest /absolute/path/project.manifest.json \
--output /tmp/project-plan.jsonApply only after reviewing the plan:
ytstudio-safe publish-project \
--session-name youtube-sandbox \
--manifest /absolute/path/project.manifest.json \
--state /absolute/path/project.state.json \
--apply --confirm 'PROJECT:project.manifest.json'The state file is replaced atomically after every irreversible step. A rerun
reuses completed uploads and skips completed A/B, scheduling, related-video,
info-card, and end-screen operations. See
examples/publish.example.json for
the complete schema; treat it as a dry-run example and replace all paths and
IDs before applying it.
Normal apply commands use direct httpx requests after session capture. The
transport bootstraps Studio, derives a fresh SAPISIDHASH, checks the active
channel, and sends the operation described by a fingerprinted endpoint spec in
specs/2026-07-22/.
If a Studio change temporarily breaks direct transport, an individual command
can explicitly use the dedicated browser profile with --browser-fallback.
There is no automatic fallback, and the same channel guard still runs.
The client does not fabricate or bypass Google attestation. If Studio requires an attestation the client cannot obtain legitimately, the request fails closed.
Run all offline tests and lint checks:
./run_dev.sh
uv run --with ruff --with-editable . ruff check .The automated test suite must not contact Google or YouTube. Live validation
belongs on a disposable test channel and always requires explicit --apply.
Licensed under GPL-3.0. See LICENSE. This code began as a heavily
modified derivative of yusufusta/ytstudio.
Upstream revisions and other comparative references are documented in
THIRD_PARTY_NOTICES.md.
This project is not affiliated with, endorsed by, or supported by YouTube or Google.