Building a LinkedIn-publishing agent skill in Claude Code
- Jesús Rojo Martínez
- 10 May, 2026
I write LinkedIn posts in Markdown (and yes, AI helps me write them as well). I tag people, drop in screenshots, schedule for the morning, sometimes link to an SVG diagram I made in Mermaid. The posting itself is the boring part: open LinkedIn in a browser, paste in chunks, fix the formatting LinkedIn ate, attach the images one at a time, click Post.
I wanted that step automated. Not talking now about the writing: Just the publishing (and all the work around it).
There are commercial tools for this: Buffer, Hootsuite, Typefully, Publer. They use the official LinkedIn API, charge per post and need OAuth access to my account. I didn’t want any of that. I wanted Claude Code to drive my own browser, replicate the actions I’d take myself and stop when I told it to stop. I wanted to go through the full skill creation process for learning purposes.
This article is about how that became a Claude Code skill called linkedin-post. What it does, what it deliberately doesn’t and the longer story of how it got built. There was a moment in the process where the only way forward was to put a human’s hand on a real mouse.
Table of contents
Open Table of contents
- What it is, and what it isn’t
- Why drive a browser instead of using the API
- How I started: empirically, not from docs
- The product decisions, not the technical ones
- The file picker
- A few other gotchas, faster
- Architecture: a main agent that doesn’t do the dirty work
- What works today
- What it does not do
- What’s next
What it is, and what it isn’t
The skill is a runbook. You point it at a Markdown file and tell it what to do:
Post
linkedin-post.mdto LinkedIn.
Schedule
linkedin-post.mdfor tomorrow at 10am.
Draft
linkedin-post.mdand let me review before posting.
It reads the file, parses the body, converts the formatting into something LinkedIn actually renders (more on that), opens the LinkedIn composer in a browser session you’ve already logged into, fills everything in, attaches images and either posts, schedules or stops at the unposted modal so you can click Post yourself. Think of Claude Computer (and the like), but a tad more efficient (works with the DOM, not with screenshots).
What it is not: a content generator. The post body comes from your file. There is no --prompt "write me a clever post about...". For help writing the post, use a writing skill or a writing tool. This one publishes.
It also doesn’t do video, document attachments, polls, articles (LinkedIn’s long-form), comments, reactions, DMs or any kind of cross-posting. It is a single, narrow tool. That was deliberate.
Why drive a browser instead of using the API
LinkedIn does have an API. You can post to it. You can schedule via it. There’s a real path.
For me, to be honest, the main reason was learning. I did want to have the experience of creating a complex skill from scratch. And to add to that, other reasons were:
- Cost. Most third-party tools that wrap the API charge per post or sit behind paywalls or have limited free-tiers.
- Approval. Creating a tool using the API requires registering it in LinkedIn and getting write-post permission. This is a multi-week loop with uncertain outcome for personal use.
Driving the browser as a real user gets around all of that. The cost is that the AI is interpreting what it sees on the page instead of making clean API calls, which uses more tokens per post. I mitigate this by having the skill read structured page descriptions instead of full screenshots, and by keeping the noisy automation work in a separate subagent so the main conversation context stays small.
There’s also fragility: if LinkedIn changes the composer’s UI, the skill breaks until I update it. I accept that. The runbook lives in one file so patching it is a quick edit, and the gotchas are documented in plain English so future-me knows what to look at.
What this approach doesn’t do is ask me for OAuth, charge me per post or expose me to a third party who now has a write token to my LinkedIn account. For a few-times-a-week posting cadence and a personal account, the tradeoff goes the right way. For high-volume social-media management at a company, the tradeoff goes the other way and you should use the API tools.
This skill is not trying to be the right answer for everyone. It’s a working answer for one case: someone who writes in Markdown, trusts a logged-in browser more than an OAuth grant and would rather pay in (subscription) tokens than money. It’s also way cheaper than something like Claude Computer, since Claude Computer navigates via literal screenshots while this skill reads the page structurally.
How I started: empirically, not from docs
The first thing I did wasn’t write code. It was open the LinkedIn composer in a Claude-driven browser and start poking.
Claude Code probed each capability using agent-browser before writing any skill code. Can Claude type bold text? (Not directly. LinkedIn doesn’t render Markdown, but there are Unicode characters that look bold and survive submission.) Can it attach an SVG? (No. LinkedIn rejects it, even after the file lands in the upload field.) Can it create a real @mention? (Yes, but only if it types letter by letter and lets LinkedIn’s autocomplete dropdown appear. Pasting @username in one shot leaves it as plain text.)
I treated LinkedIn as a black box and reverse-engineered its rules through tests, not docs. Every gotcha I found went straight into a memory file. By the time I started writing the skill, that file was the source of truth, and the skill’s runbook is now mostly a copy of it with the relevant commands added.
That order matters. If I’d started by writing code, I’d have spent a long time being surprised by LinkedIn. Probing first meant the surprises arrived before I’d locked in any architecture to be surprised about.
The product decisions, not the technical ones
A bunch of choices shape what it’s like to use this skill. None of them are technical. They’re product decisions, and they’re more important than the implementation.
Where the post body lives
A Markdown file usually has more than one thing in it: frontmatter, your post body, maybe some authoring notes you don’t want published. I needed a clean separator.
The convention is markers:
---
title: My post
linkedin:
schedule: "+30m"
---
Authoring notes for myself or for the skill go here. The skill scans this region for free-form directives like "no preview" or "use the matchamplified link as the preview."
<!-- linkedin:start -->
This is the actual post body that gets published.
<!-- linkedin:end -->
Anything down here is silently ignored. Even directives. Even comments. Especially future ideas you don't want accidentally posted.
I went back and forth on whether anything after linkedin:end should still be parsed for directives. I decided no. The marker is the marker. If you want the skill to do something, put it before the body, where you’d put any other instruction. Keeping the post body and the metadata cleanly separated removes a whole category of “wait, did I accidentally just publish that?” moments.
Three places parameters can come from
You can set the post’s behaviour in three places:
- The prompt you type. “Schedule for tomorrow 10am, no preview.”
- Free-form English in the directive region of the file. “Show no link previews. Schedule for tomorrow at 10am.”
- YAML frontmatter with structured keys.
Precedence is prompt → directives → frontmatter. The skill resolves them, then asks me a question for anything ambiguous before it opens the browser. By the time I see a composer fill itself in, every decision is locked.
The reason for three sources is that they serve different needs. Frontmatter is for things that belong with the post forever (a specific schedule baked into the file). Directives are for one-off English notes you can write naturally. Prompts are for last-minute overrides (“actually, schedule it later”).
Four publish modes
Two of the modes are obvious: post immediately or schedule for later. A third (dry-run) just parses the file and prints what would happen, without ever opening a browser. I use it when I’m debugging the parser.
The interesting one is review. This is the human-in-the-loop mode. The skill opens the composer, fills everything in, attaches the images, applies the preview-card override if you set one and then stops. The composer sits there with everything ready, waiting for me to click Post or Schedule manually. I click the button.
Auto-camelCase for hashtags
LinkedIn’s hashtag system terminates at the first non-letter character. If you write #AI-agents, the composer renders #AI (a real hashtag link) followed by literal -agents (plain text).
The skill normalizes hyphens and underscores to camelCase up front: #AI-agents becomes #AIAgents, #growth_hacking becomes #GrowthHacking. It surfaces a warning in the success summary so you can verify the result. This is required, not optional. LinkedIn won’t fix it for you.
Mentions go through real autocomplete
There’s a way to construct LinkedIn mention elements by hand and inject them directly. I tested it; LinkedIn accepts them at publish time and renders real profile links.
The skill doesn’t do that. It uses real keystrokes, waits for LinkedIn’s autocomplete dropdown to appear and clicks the matching person from the list.
Two reasons:
- The author shouldn’t have to look up LinkedIn’s internal IDs for every person they want to tag. That defeats the point of writing posts in Markdown.
- The internal shape of those mention elements is LinkedIn’s private business. They can change it any time. The autocomplete dropdown is part of the visible product and is much more stable.
Matching is by surname, case-insensitively, with tolerance for accented characters: typing Pawel Huryn correctly surfaces Paweł Huryn. If the dropdown doesn’t have a match for someone (because you have no LinkedIn connection to them), the skill falls back to typing the name as plain text and warns you. Your post still publishes; the mention just isn’t a real link.
The file picker
This is the part of the build Claude could not figure out on its own.
When the skill clicked “Add” to attach an image, the OS file picker popped up and stayed open. The image actually got attached — there’s a separate path that feeds the file in directly — but the file picker just sat there blocking everything until someone clicked Cancel manually.
Claude spent a long time trying to suppress it. It patched the obvious places where the click might originate. Claude confirmed the listener was firing on the right element, in the right context, with the right setup. The picker still opened.
Then I, the human in the loop watching the Claude-driven browser, clicked the “Upload from computer” button manually with my actual mouse. The picker did not open. Same code. Same listener. Same DOM. Manual click: picker suppressed. Automated click via the browser-driver: picker opened.
The difference turned out to be how Chromium classifies the events. Clicks coming from automation get treated specially and trigger the picker before any JavaScript can stop them. Clicks coming from a real human don’t. There’s no way to ask the browser-driver to send a “less trusted” click. The fix was to skip the click entirely and feed the file in through a separate path that doesn’t involve clicking the button at all.
A few other gotchas, faster
LinkedIn rejects SVG. Even when you bypass the file picker entirely, LinkedIn’s frontend rejects anything outside a small list of accepted formats with a “Something went wrong” toast. The skill rasterizes SVG, HEIC, AVIF, BMP and TIFF to PNG before upload.
Autocomplete dropdowns are flaky. A naive “click the option by its name” approach races against the dropdown refreshing while Claude is still typing. The fix is to find the matching option directly in the page rather than going through the abstraction the browser-driver normally uses.
Some buttons aren’t real buttons. LinkedIn’s primary action buttons are styled <div> elements that behave like buttons but aren’t. Anything that searches for actual <button> elements misses them. It took a while for Claude to figure that one out.
These are the kinds of things you only learn by trying and testing.
Architecture: a main agent that doesn’t do the dirty work
The skill splits work between two agents.
The main agent runs in your conversation. It reads your prompt, reads the file, applies the parameter precedence, asks for clarification if anything is ambiguous and packages everything into a self-contained brief.
It then dispatches a subagent with that brief and a one-line instruction: read the runbook, execute every step, return a summary.
The subagent does all the noisy work. Image conversion. Browser session checks. Composer open. Body typing. Image uploads. Schedule or post or stop-at-review. Pop-up dismissal. Hundreds of small automated steps. None of it leaks into your conversation.
When the subagent returns, it gives the main agent a small JSON: did it succeed, what was the post URL or scheduled time, what warnings happened, where’s the screenshot, where’s the temp directory, did it clean up the temp directory. The main agent formats that for the user and stops.
The split is what makes the skill usable in long conversations. Without it, every invocation would dump tens of kilobytes of intermediate state into the chat history and there’d be nothing left for actual work.
What works today
- Publish or schedule from a local Markdown file
- Bold, italic, bold italic — formatting LinkedIn doesn’t render natively, simulated with the right Unicode characters
- Nested bullet lists with four distinct glyph levels
- Mentions matched via real autocomplete, including tolerance for accented characters
- Hashtags auto-fixed when they contain hyphens or underscores (LinkedIn breaks those otherwise)
- Inline links rewritten so the URL is readable in the post
- Image upload, including SVG and other formats LinkedIn rejects (the skill rasterizes them to PNG)
- Preview-card override (force a specific URL or suppress the card entirely)
- Four publish modes: post, schedule, review, dry-run
- Three parameter sources combined with precedence
- MDX support: any JSX components in the source file are silently stripped before parsing
What it does not do
- Video uploads
- Document attachments (PDF, PPT)
- LinkedIn long-form articles, polls or newsletters
- DMs, comments, reactions
- Multi-account posting (single LinkedIn profile per browser profile, today)
- Editing posts after they’re published
- Cross-posting to other platforms
These are listed in the skill’s TODO.md. None of them are blockers for v1; they’re future work if I or anyone else wants them.
What’s next
On the roadmap (full list, with rationale and rough effort estimates, in the skill’s TODO.md):
- npm packaging. Today the skill installs by copying the folder into a Claude skills folder and running
npm installonce. A future version would publish properly, with an installer that handles all of that. Mechanical work; the design doesn’t change. - Editing scheduled posts. The skill can schedule a post but can’t go back and tweak one. The flow exists in LinkedIn’s UI; the skill would need to navigate to the scheduled-posts page, find the right one, open it, apply edits.
- Companion skills, eventually:
linkedin-commentandlinkedin-repostwould share most of the browser plumbing with this one.
None of those change the core. The core is the runbook, the parameter resolution, the body-typing flow and the workarounds for LinkedIn’s specific quirks. Those are stable.
The skill is on my machine. Not on GitHub yet. If there’s interest in a public version, I’ll set up a repo and the npm package. Drop me a message and I’ll move that up the queue.
I’m also planning to use the skill itself to publish an upcoming series of LinkedIn posts about Match Amplified. Honestly, that’s why I built it in the first place. If you see one of those posts on your LinkedIn feed and the formatting is exactly right for once, that’s how.
Share :