hive.linkedin-automation
Read before automating LinkedIn with browser_* tools. LinkedIn combines shadow DOM (#interop-outlet), strict Trusted Types CSP that silently drops innerHTML, Lexical composer, native beforeunload dialogs that hang the bridge, and aggressive spam filters — each has bitten us at least once.
Install / Use
npx skills add aden-hive/hive --skill linkedin-automationInstalls into whichever agent you are using.
SKILL.md
Installable skill definition
Quality Score
Category
AutomationSupported Platforms
Our assessment of hive.linkedin-automation
hive.linkedin-automation scores 97/100 on our quality scale, 113th of 1,335 Automation skills we index (top 9%).
Its SKILL.md is 21 KB long, well organised into 53 sections with 7 code examples: a thorough specification that gives an agent plenty to work with.
With 11,072 GitHub stars, it is one of the more widely adopted skills in the catalogue.
Maintenance, license and trust
- The repository was last updated 12 days ago, so hive.linkedin-automation is actively maintained.
- It is released under the Apache-2.0 license, a permissive license that allows use, modification and commercial use with attribution.
- Its trust signals score 100/100, with no cautions. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.
Safety scan
No issues foundOur scan of the whole file found no instruction hijacking, hidden characters, credential access, data exfiltration or destructive commands.
Automated pattern scan on 2026-09-26. It catches known dangerous patterns, not every risk — read a skill before letting an agent act on it.
hive.linkedin-automation compared with similar skills
All 4 of these similar skills score higher than hive.linkedin-automation; compare them before choosing.
| Skill | Score | Stars | Updated | Format |
|---|---|---|---|---|
| hive.linkedin-automation (this skill)by aden-hive | 97 | 11.1k | 12d ago | SKILL.md |
| Agent-Reachby Panniantong | 100 | 85.5k | 10d ago | CLAUDE.md |
| rufloby ruvnet | 100 | 73.3k | 1d ago | CLAUDE.md |
| Scraplingby D4Vinci | 100 | 83.7k | today | MCP Server |
| algorithmic-artby anthropics | 100 | 177.9k | 3d ago | SKILL.md |
Frequently asked questions
- How do I install hive.linkedin-automation?
- Run
npx skills add aden-hive/hive --skill hive.linkedin-automation. The install tabs above show the steps for each supported agent. - Which AI agents does hive.linkedin-automation work with?
- It is written for Universal, as a SKILL.md file. Other agents that read the same format can often use it too.
- Is hive.linkedin-automation safe to use?
- Our scan of the whole file found no instruction hijacking, hidden characters, credential access, data exfiltration or destructive commands. It is Apache-2.0-licensed and scores 100/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
- Is hive.linkedin-automation still maintained?
- The repository was last updated 12 days ago, so hive.linkedin-automation is actively maintained.
Skill content
View source on GitHubname: hive.linkedin-automation description: Read before automating LinkedIn with browser_* tools. LinkedIn combines shadow DOM (#interop-outlet), strict Trusted Types CSP that silently drops innerHTML, Lexical composer, native beforeunload dialogs that hang the bridge, and aggressive spam filters — each has bitten us at least once. Verified flows for profile messaging, connection-request acceptance, feed composition, and search. Requires hive.browser-automation. Verified against logged-in production 2026-04-11. metadata: author: hive type: default-skill version: "1.0" verified: 2026-04-11 requires_skill: hive.browser-automation
LinkedIn Automation
LinkedIn is the hardest mainstream site to automate because it combines shadow DOM (#interop-outlet for messaging), strict Trusted Types CSP (silently drops innerHTML), heavy React reconciliation (injected nodes get stripped on re-render), native beforeunload draft dialogs (hang the bridge), and aggressive spam filters. Every one of those has bit us at least once. This skill documents what actually works.
Always activate browser-automation first. This skill assumes you already know about CSS-px coordinates, browser_type/browser_type_focused, and browser_shadow_query. The guidance below is LinkedIn-specific; general browser rules are there.
Rule #0: screenshot + coordinates, not selectors
LinkedIn changes class names aggressively and hides composers inside shadow roots AND iframes. Selectors break constantly. Your default strategy on every LinkedIn page should be:
browser_screenshot()— see the page visually- Pick the target's position from the image
browser_coords(image_x, image_y)→ get CSS pixelsbrowser_click_coordinate(css_x, css_y)— reaches shadow DOM, iframes, and React elements indifferentlybrowser_type(use_insert_text=True, text=...)— types into whatever is focused, including Lexical composers
If browser_evaluate(...querySelectorAll...) returns [] even once, do not try a different selector. Stop, screenshot, and click. The "what if I try .artdeco-list__item next" instinct has burned ~50 tool calls in real sessions before the agent pivoted. Don't fall into that loop.
The selectors in the table below are only for when you already know the target is in the light DOM and you want a faster path than screenshot+coord. When in doubt, default to coordinates.
Invitation manager — inline message button path is BROKEN
If the user asks to message a connection request from the invitation manager page without accepting first, the inline "Message" button opens a composer inside a nested iframe overlay (not a shadow root). The iframe's contentDocument is either cross-origin-blocked or not hydrated at access time. This path is not reliably automatable today.
Redirect: click the person's name/profile link on the card, go to the profile page, and use the standard Profile Message flow below. The profile flow is battle-tested; the inline-iframe flow isn't.
If you end up writing document.activeElement.tagName === 'IFRAME' inside a browser_evaluate, you've hit this trap. Stop and go to the profile page.
Timing expectations
browser_navigate(wait_until="load")— LinkedIn takes 4–5 seconds to load the feed cold.- After navigation, always
sleep(3)to let React hydrate the profile/feed chrome before querying selectors. Without the sleepwait_for_selectorwill flake on elements that exist moments later. - Composer modal slide-in takes ~2 seconds after you click the Message button.
Verified selectors
| Target | Selector | Notes |
|---|---|---|
| Global search input | input[data-testid='typeahead-input'] | Light DOM, straightforward |
| Own profile link | a[href*='linkedin.com/in/'] | Top nav; filter to the one near top-left |
| Profile Message action | a[href*='/messaging/compose/'] filtered by NON_SELF_PROFILE_VIEW AND no body= param AND x < 700 | Is an <a>, not a <button>. Multiple match; filter carefully. |
| Modal composer textarea | div.msg-form__contenteditable (inside #interop-outlet shadow) | Multiple instances exist — pick largest-area in-viewport one. |
| Modal Send button | button.msg-form__send-button (inside #interop-outlet shadow) | Same multi-instance trap — filter by y + height <= innerHeight. |
| Invitation manager | navigate to https://www.linkedin.com/mynetwork/invitation-manager/received/ | Direct URL is faster than nav-link clicking |
| Pending connection card | .invitation-card, .invitations-card, [data-test-incoming-invitation-card] | Filter out "invited you to follow" / "subscribe" cards |
| Accept button | button[aria-label*="Accept"] within the card scope | Per-card scoping is critical — there are many Accept buttons on the page |
LinkedIn changes class names aggressively. If a class-based selector breaks, fall back to browser_screenshot → visual identification → browser_click_coordinate with the pixel you read straight off the image (screenshots are CSS-sized, no conversion). The screenshot + coord path works regardless of class-name churn and regardless of shadow DOM.
Profile Message flow (verified end-to-end 2026-04-11)
# 1. Load the profile
browser_navigate("https://www.linkedin.com/in/<username>/", wait_until="load")
sleep(3)
# 2. Strip onbeforeunload before any state-mutating work — prevents draft-dialog deadlock later
browser_evaluate("""
(function(){
window.onbeforeunload = null;
window.addEventListener('beforeunload', e => e.stopImmediatePropagation(), true);
})();
""")
# 3. Find the profile Message link (NOT a button, and multiple exist)
msg_btn = browser_evaluate("""
(function(){
const links = Array.from(document.querySelectorAll('a[href*="/messaging/compose/"]'));
for (const a of links){
const href = a.href || '';
if (!href.includes('NON_SELF_PROFILE_VIEW')) continue;
if (href.includes('body=')) continue; // reject Premium upsell
const r = a.getBoundingClientRect();
if (r.width === 0 || r.x > 700) continue; // reject sidebar / "More profiles for you"
return {cx: r.x + r.width / 2, cy: r.y + r.height / 2};
}
return null;
})();
""")
browser_click_coordinate(msg_btn['cx'], msg_btn['cy'])
sleep(2.5) # composer modal slide-in
# 4. Find the modal composer textarea (pick biggest in-viewport; reject pinned chat bar)
textarea = browser_evaluate("""
(function(){
const vh = window.innerHeight, vw = window.innerWidth;
const candidates = [];
function walk(root){
const els = root.querySelectorAll ?
root.querySelectorAll('div.msg-form__contenteditable') : [];
for (const el of els){
const r = el.getBoundingClientRect();
if (r.width <= 0 || r.height <= 0) continue;
if (r.y < 0 || r.y + r.height > vh) continue; // reject pinned bar (below viewport)
if (r.x < 0 || r.x + r.width > vw) continue;
candidates.push({cx: r.x + r.width/2, cy: r.y + r.height/2, area: r.width * r.height});
}
const all = root.querySelectorAll ? root.querySelectorAll('*') : [];
for (const host of all){ if (host.shadowRoot) walk(host.shadowRoot); }
}
walk(document);
if (!candidates.length) return null;
candidates.sort((a, b) => b.area - a.area);
return candidates[0];
})();
""")
# 5. Click to focus the modal composer (click-first is mandatory for Lexical)
browser_click_coordinate(textarea['cx'], textarea['cy'])
sleep(0.6)
# 6. Insert text via browser_type_focused. This dispatches CDP
# Input.insertText to document.activeElement — the same underlying
# mechanism as execCommand('insertText') but with no JSON escaping,
# no browser_evaluate round trip, and built-in retry. The click in
# step 5 already focused Lexical, so insertText lands in the editor
# regardless of the shadow wrapping around #interop-outlet.
#
# Use browser_type_focused (not browser_type) here — browser_type
# requires a selector, which cannot see past the #interop-outlet
# shadow root. browser_type_focused targets document.activeElement
# directly, sidestepping shadow boundaries entirely.
browser_type_focused(text=message_text)
sleep(1.0) # let Lexical commit state + enable Send button
# 7. Find the modal Send button (filter by in-viewport, reject pinned bar)
send = browser_evaluate("""
(function(){
const vh = window.innerHeight;
function walk(root){
const btns = root.querySelectorAll ? root.querySelectorAll('button') : [];
for (const b of btns){
const cls = (b.className || '').toString();
const txt = (b.textContent || '').trim();
if (!cls.includes('send-button') && txt !== 'Send') continue;
const r = b.getBoundingClientRect();
if (r.width <= 0 || r.y + r.height > vh) continue;
return {
cx: r.x + r.width/2, cy: r.y + r.height/2,
disabled: b.disabled || b.getAttribute('aria-disabled') === 'true',
};
}
const all = root.querySelectorAll ? root.querySelectorAll('*') : [];
for (const host of all){
if (host.shadowRoot){
const got = walk(host.shadowRoot);
if (got) return got;
}
}
return null;
}
return walk(document);
})();
""")
# 8. ONLY click Send if it's enabled — if disabled, the insertText
# didn't land. DO NOT retry with a different tool; the fix is
# always: re-click the composer rect, re-run browser_type_focused(text=...),
# re-check. The Send button's `disabled` state IS the ground truth —
# if Lexical registered your text, it enables the button. If it's
# still disabled, your text did not reach the editor, regardless
# of what any tool call claims.
if send['disabled']:
# The editor didn't receive your text. Do NOT click Send. Do NOT
# fall back to browser_type with a selector (see anti-pattern in
# Common Pitfalls — selector-based type can't reach the shadow-DOM
# composer). Instead: re-click the textarea rect from step 4, wait
# a beat, re-run browser_type_focused(text=message_text) from
# step 6. If that still fails after 2 retries, bail and surface —
# the modal may have been reclaimed by a stale state or auth wall.
raise Exception("Send button disabled after insertText — editor did not receive input")
browser_click_coordinate(send['cx'], send['cy'])
sleep(2.5) # wait for send + bubble render
Verify post-send: the composer textarea should now be empty (innerText === '') and .msg-s-event-listitem__message-bubble count should have grown by 1. Walk the shadow tree via browser_evaluate to check.
Connection request acceptance flow
Daily outbound pattern — accept pending connection requests and send a templated welcome message.
browser_navigate("https://www.linkedin.com/mynetwork/invitation-manager/received/",
wait_until="load")
sleep(4)
browser_evaluate("(function(){window.onbeforeunload=null;})()")
# Scan pending connection cards — FILTER OUT follow/subscribe invitations
cards = browser_evaluate("""
(function(){
const out = [];
const cards = document.querySelectorAll('[data-test-incoming-invitation-card], .invitation-card');
for (const c of cards){
const text = (c.textContent || '').toLowerCase();
if (text.includes('invited you to follow')) continue;
if (text.includes('invited you to subscribe')) continue;
const nameEl = c.querySelector('a[href*="/in/"], strong');
const name = nameEl ? nameEl.textContent.trim().split(/\\s+/)[0] : '';
const accept = c.querySelector('button[aria-label*="Accept"]');
if (!accept) continue;
const r = accept.getBoundingClientRect();
out.push({
first_name: name,
cx: r.x + r.width/2, cy: r.y + r.height/2,
});
if (out.length >= 25) break;
Truncated for display — read the full file on GitHub.
Related Skills
Agent-Reach
85.5kGive your AI agent eyes to see the entire internet. Read & search Twitter, Reddit, YouTube, GitHub, Bilibili, XiaoHongShu — one CLI, zero API fees.
ruflo
73.3k🌊 The original agent harness. Deploy intelligent multi-player swarms, coordinate autonomous workflows, and build conversational AI systems. Features adaptive memory, self-learning intelligence, federation, vector RAG integration, and native Claude Code / Codex / Hermes and many more Integrated
Scrapling
83.7k🕷️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl! Don't be shy, join here: https://discord.gg/EMgGbDceNQ and follow here for daily tips and tricks: https://x.com/Scrapling_dev
algorithmic-art
177.9kCreating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems.
Languages
Trust signals
From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.
