agentleFS
sentineldesksentineldesk/agent/skills/youtube-skip-ad/SKILL.md

youtube-skip-ad

Skill0 starsChanged 3 months agoApache-2.0View on GitHub

Watch or record a YouTube video end to end — skipping advertisements, waiting for the video to finish without burning steps, and stopping cleanly before the next one autoplays. Read this before watching or recording anything on YouTube.

The file

Show its rules and text

We split this skill into the rules it gives an agent

15 rules. About 0 are common, 9 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈9

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Verify what you are about to click before you click it, and verify what happened afterwards.

    line 25
  2. Distinctive:

    If it is true, wait for the skip control.

    line 44
  3. Distinctive:

    Wait it out or carry on; do not start clicking around.

    line 59
  4. Distinctive:

    Before clicking, confirm the element is the skip and nothing else:

    line 61
  5. Very specific:

    Read label, and read visible and covered too: a control that is present but covered is one the click will not reach, and nothing in the …

    line 67
  6. Very specific:

    Read it twice, a couple of seconds apart.

    line 120
  7. Very specific:

    Two attempts, then stop: If the second one does not clear the ad, say what you saw — including both currentTime readings — and carry on …

    line 129
  8. Distinctive:

    Look for the one whose label is a skip.

    line 151
  9. Distinctive:

    Wait in the page instead. One step, however long it takes.

    line 164
  10. Very specific:

    If yes, poll it. If it describes a moment — ended, loaded, a click landing, a frame arriving — it needs setup.

    line 248
  11. Distinctive:

    If you got the id from a search, read the result's link and take the id from there rather than clicking through:

    line 265
  12. Very specific:

    Read checked. It is the toggle's own state and it is a boolean, so it says the same thing whatever language the page is in — the label does …

    line 284
  13. Distinctive:

    If the control genuinely is not there, carry on — you have already removed the Mix in step 1, which is the bigger half — and stop the …

    line 292
  14. Distinctive:

    If the task was only this video, close the tab as well rather than leaving a player running on a shared screen.

    line 348
  15. Very specific:

    Before starting a recording: finish what you need a terminal for, or minimise it (minimize_window), and check what is actually on screen …

    line 369

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Why this is a skill and not a tool

A youtube_skip_ad tool would bake a CSS class into a compiled binary. YouTube changes its DOM on their schedule, and the next rename would ship a tool that silently does nothing — the failure this project ranks above a crash. This is text: when a selector stops working, edit this file. No release, no rebuild.

The selectors below will rot. Treat them as the first thing to try, not as the definition of the button.

The two rules that matter

1. Only act when a skip control actually exists. A YouTube watch page is covered in clickable things. Clicking at a remembered position because an ad is probably playing is how you end up on an advertiser's site, or pausing the video, or opening a share dialog. No control, no click.

2. Never click a control that is not the skip. An ad's own call-to-action sits directly beside the skip button and is designed to be hit by mistake. Verify what you are about to click before you click it, and verify what happened afterwards.

How to do it

Ads only appear if one is playing. Check first rather than waiting blindly:

browser_element  selector: .ad-showing, .ytp-ad-player-overlay

present: false means there is no ad. Use browser_element rather than browser_eval for every check in this file: it reads one element and can do nothing else, so it is allowed under MCP_POLICY=safe and readonly, where browser_eval is refused. That matters more than it sounds — under safe this skill could once click and could not verify, which is the worst of the two.

present: false means there is no ad and there is nothing to do. Stop here.

If it is true, wait for the skip control. Skips become clickable a few seconds in, so a wait is correct and a poll is not:

browser_wait_for  selector: .ytp-skip-ad-button, .ytp-ad-skip-button, .ytp-ad-skip-button-modern
                  timeout_ms: 20000

Do not pass state: present here. The default is visible and that is the whole point: the skip button is in the page for the entire advertisement and only becomes usable when the countdown ends. Waiting for presence returns in about six milliseconds — it did, in the run that made this paragraph necessary — and hands you a control nobody can press yet.

A timeout here is a normal outcome, not a failure: it means an unskippable ad. Wait it out or carry on; do not start clicking around.

Before clicking, confirm the element is the skip and nothing else:

browser_element  selector: .ytp-skip-ad-button, .ytp-ad-skip-button, .ytp-ad-skip-button-modern

Read label, and read visible and covered too: a control that is present but covered is one the click will not reach, and nothing in the label says so.

The label should read like Skip, Skip ad, Saltar, Omitir anuncio — the page's language follows the browser. If the label mentions the product, the advertiser, "Visit", "Learn more", "Shop" or a URL, it is the wrong element: do not click it, and report what you found so the selector here can be fixed.

Then click by selector — never by coordinates, which land wherever the layout happens to have moved:

browser_click  selector: .ytp-skip-ad-button, .ytp-ad-skip-button, .ytp-ad-skip-button-modern

Use browser_click. Do not click from browser_eval. They are not two ways of doing the same thing, and the wrong one fails silently — which is why this paragraph exists rather than just the line above.

el.click() in JavaScript dispatches exactly one event: click. The YouTube player does not listen for that. It listens for the pointer sequence a person makes, and browser_click sends all three through the browser's own input layer:

mouseMoved  →  mousePressed  →  mouseReleased

It also refuses when something is covering the element, instead of clicking whatever is on top. browser_eval gives you none of that, and returns your own string back, so a click that did nothing reads exactly like a click that worked.

This is not hypothetical. It was measured here: four el.click() calls on a skip button, every one returning clicked, and the ad ran to its full 30 seconds and expired on its own. The run reported success and skipped nothing.

Confirm it worked, because a click that hit nothing looks identical to one that worked:

browser_element  selector: .ad-showing, .ytp-ad-player-overlay

present: true means the ad is still playing. Before assuming a second ad started, check whether the first one ever stopped — the two look the same from the outside and lead to opposite actions:

browser_element  selector: video

Its media block carries currentTime, duration and paused.

Read it twice, a couple of seconds apart. If currentTime advanced by about the wall-clock time that passed, the ad is playing normally and your click did nothing — the same ad, not a new one. A skip that worked resets currentTime to 0 or moves it to the main video, whose duration is different. A duration that has not changed between the two reads is the same ad instance.

Clicking again will not help: what failed is the mechanism, not the timing. Report it, and check you used browser_click and not browser_eval.

Two attempts, then stop. If the second one does not clear the ad, say what you saw — including both currentTime readings — and carry on with the video. An unskippable ad ends by itself in under a minute; a loop of clicks does not end at all, and burns the run's turns waiting for something that is not coming.

When the selectors stop working

They will. Find the real one instead of guessing:

This is the one place that needs browser_eval, because finding an unknown selector is exactly the open-ended question a narrow tool cannot answer. Under MCP_POLICY=safe or readonly it will be refused — say so and hand the problem to a person rather than guessing at class names.

browser_eval
  Array.from(document.querySelectorAll('button, [role=button]'))
    .filter(e => e.offsetParent !== null)
    .map(e => ({cls: e.className, label: (e.innerText || e.getAttribute('aria-label') || '').trim()}))
    .filter(x => x.label)

Look for the one whose label is a skip. Use it, tell the person the selector changed, and put the new one in this file.

Watching something all the way through

This is the part that goes wrong, and it goes wrong the same way every time.

Never wait by looping. Reading the clock costs a STEP, so checking every fifteen seconds spends twelve steps to learn one fact, and a run has a limited number of steps. A real run was cut off at its limit with the video still playing and the recording still going — it had done everything right except this.

Wait in the page instead. One step, however long it takes.

But ended cannot be polled for, and this is the thing to get right.

browser_wait_until tests the condition every 250ms, so the condition has to still be true when it looks. ended is an INSTANT, not a state: it is true from the moment the video finishes until the page moves on, and YouTube moves on in milliseconds — it autoplays the next thing in the mix. The poll lands either side of it and sees false both times.

This is measured, not theoretical. A run waited five minutes on exactly that condition. During those five minutes the video it was recording finished, an advertisement played, and a completely different video started. The wait returned {"ok":false,"value":"false"} and the agent reported the video had never ended — while the tab had moved to watch?v=…&list=RD…&index=2, which is the second item of a Mix.

So latch the event into a flag, with setup, and poll the flag:

browser_wait_until
  setup:      window.__ended = false;
              document.querySelector('video')
                .addEventListener('ended', () => { window.__ended = true }, {once: true})
  expression: window.__ended
  timeout_ms: 600000

Write the expression as the bare flag, not window.__ended === true. Both wait correctly; only one of them can tell you why it gave up. value in the result is what the EXPRESSION evaluated to, so window.__ended === true reports "false" whether the flag is false or was never created — and those are the two cases you most need to tell apart. The bare flag reports "undefined" for the second. Measured, not assumed: the comparison form returned "false" for a flag that did not exist.

setup runs once before the waiting starts. The listener catches the instant; the flag survives it; the poll finds the flag. One step, and it cannot be missed.

If the page reloads, the flag and the listener go with it. A window property does not survive a navigation, so a wait that outlives one is polling a value that no longer exists — and undefined === true is false, quietly, until the timeout.

You can tell the two apart — provided you wrote the expression as the bare flag above — and this is why the result carries value:

{"ok":false,"value":"false"}       still playing — the flag is there, not set yet
{"ok":false,"value":"undefined"}   the page went out from under you

undefined means the listener is gone and you do not know what happened while you were not looking. Do not simply wait again: find out where you are — browser_tabs for the url, browser_element selector: video for the clock — and say so if a recording was running, because whatever played during that gap is in it.

Then check you are still watching the same video. A wait that returned may have returned on the right video or on its replacement:

browser_tabs

The url carries v=<id>. If it is not the id you started on, autoplay took over and everything since is somebody else's video.

It returns {ok, waited_ms, value}. ok: false means the timeout came first, and value says where things stood — including the message if the expression threw, which is how you find out a selector is wrong rather than getting a bare timeout.

A condition that IS a state needs no setup, because it is still true when the poll arrives:

!document.querySelector('.ad-showing')          an advertisement ending
document.querySelector('video').currentTime > 90    passing a point
document.querySelector('video').paused === false    playback actually starting

The test for which one you have: would this still be true a second later? If yes, poll it. If it describes a moment — ended, loaded, a click landing, a frame arriving — it needs setup.

Recording something end to end

The order matters and each step is here because it was got wrong.

1. Open the video by its id, not by clicking a search result.

Clicking a result usually lands you on a Mix: the url comes out as watch?v=<id>&list=RD<id>&index=1, and list=RD… is what makes YouTube chain into the next song the moment yours ends. Go there directly instead:

browser_goto   https://www.youtube.com/watch?v=<id>

If you got the id from a search, read the result's link and take the id from there rather than clicking through:

browser_element  selector: a#video-title

href is the resolved url — watch?v=<id> with whatever else the listing attached. Use only the v= part.

2. Turn autoplay OFF before you record anything.

This is the difference between a take that ends with your video and one that runs into an advertisement and somebody else's. The toggle is in the player:

browser_element  selector: .ytp-autonav-toggle-button

Read checked. It is the toggle's own state and it is a boolean, so it says the same thing whatever language the page is in — the label does not, and a skill written against an English label fails on a Spanish page. checked: true means autoplay is on: click it, and read it again to confirm it went off.

checked absent means the element is not a toggle, which usually means the selector has rotted. Say so rather than clicking it anyway.

If the control genuinely is not there, carry on — you have already removed the Mix in step 1, which is the bigger half — and stop the recording promptly at the end.

3. Get the page ready BEFORE you record. Skip any ad, then put the player back to the start and PAUSE it:

browser_eval   const v = document.querySelector('video'); v.pause(); v.currentTime = 0; v.paused

YouTube autoplays. Without this the video is already a minute in by the time recording starts, and the take begins in the middle — which is what happened, and the agent noticed too late to fix it.

4. Start recording, then start playback. In that order, or the first seconds are missing.

For a video somebody is going to WATCH, record it clean and record only the browser:

start_recording  clean: true  window: Chromium

clean leaves the mouse cursor and the name tags that say who is driving out of the take — they come back by themselves when the recording stops. window means nothing that opens elsewhere on the screen can appear in the frame. Both are off by default, because an ordinary recording is a record of work and the pointer is half of what happened.

Say you are doing it: hiding the name tags hides them from anyone watching live, for as long as it records.

start_recording
browser_click   selector: .ytp-play-button      (or browser_eval: document.querySelector('video').play())

5. Wait for the end in one step, latching ended with setup — see above. A poll cannot see ended on its own, and this is the step where that has cost a whole take.

6. Stop the recording, and stop the page. Both. A recording left running is the failure a person has to notice and fix by hand:

stop_recording
browser_eval   const v = document.querySelector('video'); v.pause(); 'paused'

Stop the video before anything else runs. YouTube does not stop at the end of a video: it plays an end screen, and then it autoplays the next thing in the mix — often after another advertisement. Everything after ended is somebody else's video, and if the recording is still going it is in the take.

If the task was only this video, close the tab as well rather than leaving a player running on a shared screen.

Before you report, check which video you are on. browser_tabs gives the url; if v= is not the id you started with, the take contains material you did not intend and you have to say so. Reading duration off the player and comparing it with the length the listing advertised catches the same thing: a run reported "~1:20 of 4:27" for a video that is 3:34 long, because it was reading a different video and never noticed the numbers disagreed.

7. Say where the file went. stop_recording returns the path and the size. An agent that recorded something and did not say where it is has produced a file nobody can find.

Your own windows are in the shot

The screen recording records the SCREEN, including anything you open. A terminal opened by run_command lands in a visible window on the shared desktop — that is deliberate, so a person can see what is being run — and it will sit on top of whatever you are recording.

Before starting a recording: finish what you need a terminal for, or minimise it (minimize_window), and check what is actually on screen with list_windows. Afterwards, check the recording is of what you meant to record and not of your own tooling.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.