grdev.youtube-cli
0.3.1
See the version list below for details.
dotnet tool install --global grdev.youtube-cli --version 0.3.1
dotnet new tool-manifest
dotnet tool install --local grdev.youtube-cli --version 0.3.1
#tool dotnet:?package=grdev.youtube-cli&version=0.3.1
nuke :add-package grdev.youtube-cli --version 0.3.1
youtube-cli
Upload a video to YouTube from the command line — set its title, description, tags and category, and either publish it at a chosen visibility or schedule it to go public later.
It is built to be driven by an AI agent as much as by a person: nothing prompts, results go to stdout (optionally as JSON), progress goes to stderr, and the exit code tells you what happened.
dotnet tool install --global grdev.youtube-cli
Before the first run
The tool uploads to your channel using your Google Cloud OAuth client. It does not ship one, because YouTube's daily quota belongs to the Cloud project — a shared client would mean every user on earth splitting roughly six uploads a day.
Setting one up is a one-off, and free:
- At console.cloud.google.com, create or pick a project.
- APIs & Services → Library → enable YouTube Data API v3. Without this every call returns a 403 that reads like a permissions problem.
- OAuth consent screen →
Internalif your channel is in a Google Workspace domain you control, otherwiseExternal. Add the scopes.../auth/youtube.uploadand.../auth/youtube.readonly. On an External app, add your own account under Test users — without it consent fails withaccess_denied. - Credentials → Create credentials → OAuth client ID → Desktop app. The type matters: consent completes on a loopback listener, which only Desktop clients allow implicitly.
- Download JSON and save it as
C:\Users\<username>\.grdev.youtube-cli\client-secret.json(~/.grdev.youtube-cli/client-secret.jsonelsewhere). That is the whole of configuration.
Then, once per machine:
youtube auth login
An unverified app shows "Google hasn't verified this app" → Advanced → Go to …. That warning is about your own unpublished app.
Do not leave the Cloud app in "Testing". Google invalidates refresh tokens after 7 days for an External app in Testing, so an unattended tool starts failing with exit
3every week for no visible reason. UseInternaluser type, or publish the External app (Audience → Publish app) — publishing unverified is fine for personal use.
Use
Describe the video in a markdown file beside it — talk.mp4 is described by talk.md:
---
Title: "Shipping a CLI in an afternoon"
Category: Science & Technology
Visibility: Private
Tags: dotnet, cli, shipping
ForKids: false
---
Everything below the marker becomes the description, exactly as written.
youtube upload talk.mp4 --DryRun # resolve everything, print it, send nothing
youtube upload talk.mp4 # upload
youtube list # what's on the channel, newest first
youtube list --Scheduled # what's booked to publish, soonest first
youtube auth status # which channel am I authorized for?
youtube version
youtube list reports each video's id, title, privacy, date and lifetime view, like and
comment counts. It reports facts only — working out where the gaps in a schedule are, or
which date to use next, is left to whoever is reading, because that depends on a cadence and
a plan the tool has no business guessing.
Any option overrides the file — youtube upload talk.mp4 --Visibility Public. With no
sidecar, pass everything as options instead.
Sidecar fields
| Field | Value | Default |
|---|---|---|
Title |
Text, max 100 characters | — required |
Category |
A category name (Gaming, Education, …) or a numeric id |
People & Blogs |
Visibility |
Public, Unlisted or Private |
Private |
PublishAt |
ISO 8601 timestamp; implies private until then | none |
ForKids |
true or false |
false |
Tags |
Search keywords, comma-separated or a YAML list, 500 characters total | none |
The body is the description — there is no Description field. An unrecognised field is an
error rather than a silently ignored line, so a typo cannot upload an untitled video.
Quote any value containing #, or YAML will read it as a comment and truncate it silently:
Title: "5 ways to say no #shorts".
Options
Option names are shown in PascalCase but matched loosely — --PublishAt, --publishat and
--publish-at are the same option.
upload takes --File, --Title, --Description, --Category, --Visibility,
--PublishAt, --ForKids, --Tags, --DryRun. list takes --Scheduled and --Limit.
--Json works on everything.
Driving it from an agent
The tool carries its own agent guide. Installing it is two commands:
dotnet tool install --global grdev.youtube-cli
youtube skill > ~/.claude/skills/youtube-upload/SKILL.md
youtube skill prints a ready-to-use skill file — the sidecar format, the exit-code
contract, and the failure modes an agent needs to handle. Add --Json to get the same
content as a field alongside its suggested path, if the agent would rather place the file
itself.
Every command is non-interactive and safe to run unattended, except auth login, which
is a browser consent flow a human must complete once. After that an agent can upload
indefinitely without anyone present.
Branch on the exit code:
| Code | Meaning | What to do |
|---|---|---|
0 |
Done | stdout has the video id and its watch and Studio URLs |
1 |
Failed | Read stderr; YouTube's own reason is reported verbatim |
2 |
Usage error | Fix the sidecar or the options; nothing was sent, no quota spent |
3 |
Not authorized | A human must run youtube auth login |
4 |
Quota exhausted | Stop for the day — retrying cannot succeed |
Add --Json for a single JSON object on stdout. Use --DryRun first: uploads are
irreversible and cost roughly a sixth of the default daily quota each, so confirming the
merge before spending it is nearly free.
Uploads default to private. Publishing is effectively irreversible, so Public is always
something the caller asked for explicitly.
Licence
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.