A LinkedIn CLI for AI agents — unofficial reads and official OAuth publishing, cleanly separated
한국어 · English
linkedin-cli separates two surfaces clearly:
read.*: unofficial read workflows that use your own authenticated LinkedIn web session.post.*: official write workflows that use LinkedIn OAuth and official LinkedIn APIs.
Tags: linkedin, cli, sns-json-v1, unofficial-read, official-post, personal-workflow, oauth, comments, reactions, media
This project is not affiliated with LinkedIn. Read commands use unofficial web behavior and may break when LinkedIn changes its internal endpoints. Review the Terms/ToS that apply to your account; compliance is the user's responsibility.
Read:
- Read your home feed.
- Read saved posts.
- Fetch a profile.
- Search people and posts.
- Fetch posts from a profile.
- Inspect one activity.
- Read comments on an activity.
- Read reactions on an activity.
- Emit
sns-json-v1JSON for agents, scripts, and the SNS CLI ecosystem.
Write:
- Dry-run official post payloads before publishing.
- Publish text posts through the official LinkedIn Posts API.
- Publish one local image through LinkedIn Images + Posts APIs.
- Publish multi-image posts with 2-20 local images.
- Publish one local MP4 video through LinkedIn Videos + Posts APIs.
- Publish PDF/DOC/DOCX/PPT/PPTX documents through LinkedIn Documents + Posts APIs.
- Publish non-sponsored polls through the Posts API.
- Publish article/link posts.
- Reshare existing posts.
- Create replies with the official Comments API through
post reply. - Update post commentary.
- List, retrieve, create, update, and delete comments through the official Comments API.
- List, retrieve, create, and delete reactions through the official Reactions API.
- Retrieve social metadata and open/close comments through the official Social Metadata API.
- Expose Social Metadata API results through the
insights.mediacontract. - Expose Organization Share Statistics API results through the
insights.organizationcontract. - Return a structured
unsupportedenvelope for personal account-levelinsights.user. - Retrieve one post or list posts by author when the token has the required read permission.
- Delete your own official posts by share/ugcPost URN, numeric share id, or feed update URL.
- Unsave saved posts.
- Keep legacy browser fallback commands for react, unreact, save, unsave, comment, and old-style posting.
pip install agent-linkedin
# or
uv tool install agent-linkedinThe agent-linkedin package provides the linkedin-cli command. (The PyPI name differs because linkedin-cli was already taken.)
From source:
git clone https://github.com/ai-native-engineer/linkedin-cli.git
cd linkedin-cli
uv sync --extra devFrom a clone, prefix commands with uv run (e.g. uv run linkedin-cli --help), or run uv tool install . to put linkedin-cli on PATH and use the examples below as-is.
Install Playwright only if you need browser fallback behavior:
uv run playwright install chromiumThe examples below assume
linkedin-cliis installed. If you are developing from a clone, prefix each command withuv run(uv run linkedin-cli ...), or runuv tool install .to put it on PATH.
Check the CLI:
linkedin-cli --helpRead commands need a LinkedIn web session. The easiest path is to capture cookies automatically from a logged-in browser.
linkedin-cli auth login
linkedin-cli auth-statusauth login extracts cookies from a logged-in browser (Chrome, Chromium, Brave, Edge, Firefox), saves them to a private file (~/.config/linkedin/cookies.env, mode 600), and verifies the session. If automatic extraction fails, it prints manual DevTools steps — see Read Authentication. read feed and the saved-post browser fallback do not rely only on LinkedIn cookies in a Python HTTP client; they read inside the saved Playwright browser state/profile.
If automatic extraction succeeds but LinkedIn Voyager rejects the session with self-redirect/authwall behavior, capture a fresh web session directly:
linkedin-cli auth login --via-browser --browser chrome
linkedin-cli auth login --via-browser --browser firefoxFirefox requires the Playwright Firefox build first: uv run playwright install firefox.
This opens a Playwright browser window, lets you complete login/2FA/checkpoints yourself, and saves the full LinkedIn cookie jar plus browser state to private files. Cookie values are never printed. auth-status uses direct HTTP diagnostics, so it may differ from the browser-context read feed path.
Check official OAuth permissions without mutating LinkedIn:
linkedin-cli auth permission-check --json
linkedin-cli auth permission-check --post-id urn:li:ugcPost:1234567890 --jsonThen run read commands:
linkedin-cli read feed --limit 10 --json
linkedin-cli read feed --limit 10 --comments 1 --json
linkedin-cli read saved --limit 10 --json
linkedin-cli read profile your-handle --json
linkedin-cli read profile-posts your-handle --limit 5 --json
linkedin-cli read activity urn:li:activity:1234567890 --json
linkedin-cli read comments urn:li:activity:1234567890 --limit 20 --json
linkedin-cli read reactions urn:li:activity:1234567890 --limit 20 --json
linkedin-cli read search "AI engineer" --limit 10 --jsonWrite commands need an official OAuth token:
linkedin-cli post text --text "hello from linkedin-cli" --visibility public --dry-run --json
linkedin-cli post text --text "hello from linkedin-cli" --visibility public --dry-run --json --output tmp/linkedin-post-text-dry-run.json
linkedin-cli post text --text "hello from linkedin-cli" --visibility public --json
linkedin-cli post media --text "hello with image" --media image.png --visibility public --json
linkedin-cli post multi-image --text "hello album" --media one.png --media two.jpg --dry-run --json
linkedin-cli post video --text "hello video" --video clip.mp4 --title "Demo" --dry-run --json
linkedin-cli post document --text "hello deck" --document deck.pdf --title "Deck" --dry-run --json
linkedin-cli post poll --text "vote" --question "Pick one" --option Red --option Blue --duration three-days --dry-run --json
linkedin-cli post article --text "read this" --url https://example.com/post --dry-run --json
linkedin-cli post reshare urn:li:share:1234567890 --text "worth reading" --dry-run --json
linkedin-cli post quote urn:li:share:1234567890 --text "worth reading" --dry-run --json
linkedin-cli post reply urn:li:ugcPost:1234567890 --text "great post" --dry-run --json
linkedin-cli post repost urn:li:share:1234567890 --dry-run --json
linkedin-cli post update urn:li:share:1234567890 --text "updated text" --dry-run --json
linkedin-cli post get urn:li:share:1234567890 --json
linkedin-cli post list --limit 10 --json
linkedin-cli post delete urn:li:share:1234567890 --dry-run --json
linkedin-cli post delete urn:li:share:1234567890 --json
linkedin-cli comment list urn:li:ugcPost:1234567890 --json
linkedin-cli comment create urn:li:ugcPost:1234567890 --text "great post" --dry-run --json
linkedin-cli comment create urn:li:ugcPost:1234567890 --text "great post" --dry-run --json --output tmp/linkedin-comment-create-dry-run.json
linkedin-cli comment update urn:li:ugcPost:1234567890 987654321 --text "updated comment" --dry-run --json
linkedin-cli comment delete urn:li:ugcPost:1234567890 987654321 --dry-run --json
linkedin-cli reaction create urn:li:ugcPost:1234567890 --type like --dry-run --json
linkedin-cli reaction create urn:li:ugcPost:1234567890 --type like --dry-run --json --output tmp/linkedin-reaction-create-dry-run.json
linkedin-cli reaction delete urn:li:ugcPost:1234567890 --dry-run --json
linkedin-cli social metadata urn:li:ugcPost:1234567890 --json
linkedin-cli social metadata urn:li:ugcPost:1234567890 --json --output tmp/linkedin-social-metadata.json
linkedin-cli social comments-state urn:li:ugcPost:1234567890 --state closed --dry-run --json
linkedin-cli social comments-state urn:li:ugcPost:1234567890 --state closed --dry-run --json --output tmp/linkedin-comments-state-dry-run.json
linkedin-cli insights media urn:li:ugcPost:1234567890 --json
linkedin-cli insights organization urn:li:organization:123456 --json
linkedin-cli insights user --json
linkedin-cli insights user --json --output tmp/linkedin-insights-user.jsonFor longer generated posts, prefer files:
linkedin-cli post text --text-file draft.md --visibility public --dry-run --json
linkedin-cli post text --text-file draft.md --visibility public --jsonOfficial post.* commands require a LinkedIn Developer app and an access token with w_member_social.
Official comment.*, reaction.*, and social.* commands may require additional permissions such as w_member_social_feed, r_member_social_feed, w_organization_social_feed, or r_organization_social_feed, depending on your LinkedIn app/product approval.
Open the LinkedIn Developer Portal:
https://www.linkedin.com/developers/apps
Create an app and complete the required fields:
- App name
- LinkedIn Page
- Privacy policy URL
- App logo
- API Terms agreement
If you do not already have a LinkedIn Page, create or select the default Page LinkedIn allows for individual developers.
In the app's Products/Auth settings, make sure the app can request:
openidprofileemailw_member_social
The CLI uses openid profile email to identify the authenticated member and w_member_social to create, modify, and delete posts on that member's behalf.
Comments, reactions, and social metadata commands need LinkedIn Social Feed permissions. If the token does not have them, the CLI returns a permission_denied JSON envelope.
In the app's Auth tab, add the local callback URL used by the CLI:
http://localhost:8787/callback
Optional extra callback if you want to override the host:
http://127.0.0.1:8787/callback
The redirect URI must match exactly. If you pass --redirect-uri or set LINKEDIN_REDIRECT_URI, add that exact value to the app settings.
Copy the app credentials from the Auth tab.
Environment variable option:
export LINKEDIN_CLIENT_ID='...'
export LINKEDIN_CLIENT_SECRET='...'Run the local OAuth flow:
linkedin-cli auth oauth-loginUseful options:
linkedin-cli auth oauth-login --json --output tmp/linkedin-auth-oauth-login.json
linkedin-cli auth oauth-login --timeout 300
linkedin-cli auth oauth-login --no-open
linkedin-cli auth oauth-login --redirect-uri http://localhost:8787/callbackThe command opens LinkedIn OAuth, validates the callback state, fetches the authenticated member, and writes:
~/.config/linkedin/oauth.json
Token file shape:
{
"access_token": "...",
"author_urn": "urn:li:person:...",
"linkedin_version": "202605"
}Keep this file private. The CLI expects it to be readable only by your user.
Always dry-run first:
linkedin-cli post text --text "token smoke test" --visibility public --dry-run --jsonThen publish only when the text is final:
linkedin-cli post text --text-file draft.md --visibility public --jsonDelete by the returned post id:
linkedin-cli post delete urn:li:share:1234567890 --dry-run --json
linkedin-cli post delete urn:li:share:1234567890 --jsonOops. We can't verify the authenticity of your request because the state parameter was modified.
- Restart
linkedin-cli auth oauth-login. - Do not reuse an old OAuth URL.
- Complete the flow in the browser tab opened by the CLI.
- Check that the redirect URI in the Developer Portal exactly matches the CLI redirect URI.
- If a stale localhost callback page is open, close it and retry.
permission_denied or missing w_member_social
- Confirm the app has the Share on LinkedIn / member social product enabled.
- Re-run
auth oauth-loginafter the product/scope is enabled. - Confirm the OAuth consent screen shows
w_member_social. - For comments, reactions, and social metadata, confirm whether
w_member_social_feed/r_member_social_feedor organization social feed permissions are required.
auth_expired
- Re-run
linkedin-cli auth oauth-login.
Official references:
- LinkedIn OAuth 2.0 Authorization Code Flow: https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow
- Share on LinkedIn: https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/share-on-linkedin
- LinkedIn Posts API: https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api
- LinkedIn MultiImage Post API: https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/multiimage-post-api
- LinkedIn Videos API: https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/videos-api
- LinkedIn Documents API: https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/documents-api
- LinkedIn Poll API: https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/poll-post-api
- LinkedIn Comments API: https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/comments-api
- LinkedIn Reactions API: https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/reactions-api
- LinkedIn Social Metadata API: https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/social-metadata-api
Read authentication is separate from official write OAuth.
Resolution order:
LINKEDIN_COOKIE_HEADERLINKEDIN_LI_AT+LINKEDIN_JSESSIONIDLINKEDIN_COOKIE_FILEor the default~/.config/linkedin/cookies.env- Browser cookie extraction from Chrome, Chromium, Brave, Edge, or Firefox
Recommended: capture automatically with auth login. It extracts cookies from a logged-in browser, saves them to a private file (mode 600), and verifies the session. Cookie values are never printed.
linkedin-cli auth login
linkedin-cli auth-statusIf the extracted cookies are rejected by LinkedIn Voyager, capture a fresh web session in a Playwright browser window:
linkedin-cli auth login --via-browser --browser chrome
linkedin-cli auth login --via-browser --browser firefoxFirefox requires the Playwright Firefox build first: uv run playwright install firefox.
If automatic extraction fails (on macOS, Chrome/Brave/Edge prompt for Keychain access — --browser firefox is the most reliable), capture the cookie manually:
Manual capture (DevTools):
- Open https://www.linkedin.com in your browser and confirm you are logged in.
- Open DevTools (
Option+Command+Ion macOS, orF12). - Application tab → Storage → Cookies →
https://www.linkedin.com. - Copy the values of
li_atandJSESSIONID(JSESSIONIDlooks like"ajax:..."— copy it including the quotes). - Build one line:
li_at=<value>; JSESSIONID=<value> - Save it: run
linkedin-cli auth cookie-file --from-stdin, paste the line, pressReturn, thenCtrl-D. - Verify:
linkedin-cli auth-status
Alternatively, copy the entire cookie: request header from a www.linkedin.com request in the DevTools Network tab and paste that (the fuller cookie jar LinkedIn sometimes needs).
These values are equivalent to your LinkedIn password — never paste them into chat, commit them, or share them.
One-shot env option:
export LINKEDIN_COOKIE_HEADER='li_at=...; JSESSIONID="ajax:..."; ...'
linkedin-cli auth-statusMinimal cookie variables. If authwall/checkpoint or redirects appear, use the full Cookie header instead.
export LINKEDIN_LI_AT='AQ...'
export LINKEDIN_JSESSIONID='"ajax:123456789"'Optional browser settings:
export LINKEDIN_BROWSER='chrome'
export LINKEDIN_HEADLESS='1'
export LINKEDIN_PROXY='http://127.0.0.1:7890'
export LINKEDIN_CONFIG="$PWD/config.yaml"
export LINKEDIN_COOKIE_FILE="$HOME/.config/linkedin/cookies.env"
export LINKEDIN_BROWSER_STATE="$HOME/.config/linkedin-cli/browser-state.json"Canonical JSON commands:
linkedin-cli auth-status
linkedin-cli auth oauth-login
linkedin-cli read feed --limit 20 --json
linkedin-cli read feed --limit 20 --comments 1 --json
linkedin-cli read saved --limit 20 --json
linkedin-cli read profile your-handle --json
linkedin-cli read profile-posts your-handle --limit 5 --json
linkedin-cli read activity urn:li:activity:1234567890 --json
linkedin-cli read comments urn:li:activity:1234567890 --limit 20 --json
linkedin-cli read reactions urn:li:activity:1234567890 --limit 20 --json
linkedin-cli read search "product manager" --limit 10 --json
linkedin-cli saved list --limit 20 --json
linkedin-cli saved unsave urn:li:activity:123 --dry-run --json
linkedin-cli post text --text "hello" --visibility public --dry-run --json
linkedin-cli post text --text-file draft.md --visibility public --json
linkedin-cli post media --text "hello with image" --media image.png --visibility public --json
linkedin-cli post multi-image --text "hello album" --media one.png --media two.jpg --json
linkedin-cli post video --text "hello video" --video clip.mp4 --title "Demo" --json
linkedin-cli post document --text "hello deck" --document deck.pdf --title "Deck" --json
linkedin-cli post poll --text "vote" --question "Pick one" --option Red --option Blue --duration three-days --json
linkedin-cli post article --text "read this" --url https://example.com/post --json
linkedin-cli post reshare urn:li:share:1234567890 --text "worth reading" --json
linkedin-cli post quote urn:li:share:1234567890 --text "worth reading" --json
linkedin-cli post repost urn:li:share:1234567890 --dry-run --json
linkedin-cli post update urn:li:share:1234567890 --text "updated text" --json
linkedin-cli post get urn:li:share:1234567890 --json
linkedin-cli post list --limit 10 --json
linkedin-cli post delete urn:li:share:1234567890 --dry-run --json
linkedin-cli post delete urn:li:share:1234567890 --json
linkedin-cli comment list urn:li:ugcPost:1234567890 --json
linkedin-cli post reply urn:li:ugcPost:1234567890 --text "great post" --json
linkedin-cli comment get urn:li:ugcPost:1234567890 987654321 --json
linkedin-cli comment create urn:li:ugcPost:1234567890 --text "great post" --dry-run --json
linkedin-cli comment update urn:li:ugcPost:1234567890 987654321 --text "updated comment" --dry-run --json
linkedin-cli comment delete urn:li:ugcPost:1234567890 987654321 --dry-run --json
linkedin-cli reaction list urn:li:ugcPost:1234567890 --json
linkedin-cli reaction get urn:li:ugcPost:1234567890 --json
linkedin-cli reaction create urn:li:ugcPost:1234567890 --type like --dry-run --json
linkedin-cli reaction delete urn:li:ugcPost:1234567890 --dry-run --json
linkedin-cli social metadata urn:li:ugcPost:1234567890 --json
linkedin-cli social metadata urn:li:ugcPost:1234567890 --json --output tmp/linkedin-social-metadata.json
linkedin-cli social comments-state urn:li:ugcPost:1234567890 --state open --dry-run --json
linkedin-cli insights media urn:li:ugcPost:1234567890 --json
linkedin-cli insights organization urn:li:organization:123456 --json
linkedin-cli insights user --json
linkedin-cli insights user --json --output tmp/linkedin-insights-user.jsonLegacy compatibility commands:
linkedin-cli feed --max 10
linkedin-cli search "product manager" --max 10
linkedin-cli profile your-handle --json --output tmp/linkedin-profile.json
linkedin-cli profile-posts your-handle --max 20
linkedin-cli activity urn:li:activity:123 --json --output tmp/linkedin-activity.json
linkedin-cli post "hello from browser fallback"
linkedin-cli react urn:li:activity:123 --type like
linkedin-cli unreact urn:li:activity:123
linkedin-cli save urn:li:activity:123
linkedin-cli unsave urn:li:activity:123
linkedin-cli comment urn:li:activity:123 "nice post"All canonical --json commands emit a single sns-json-v1 envelope:
{
"schema_version": "sns-json-v1",
"ok": true,
"platform": "linkedin",
"command": "post.text",
"source": "official",
"request": {},
"data": {},
"error": null,
"warnings": [],
"meta": {
"cli_name": "linkedin-cli"
}
}Secrets are never written to request, data, raw, or logs.
from pathlib import Path
from linkedin_cli import LinkedInWriteAPI
api = LinkedInWriteAPI.from_config()
plan = api.plan_text_post(text=Path("draft.md").read_text(), visibility="public")
print(plan.to_dict())
result = api.create_text_post(text=Path("draft.md").read_text(), visibility="public")
print(result.url)
delete_plan = api.plan_delete_post(post_id=result.post_id)
print(delete_plan.to_dict())
delete_result = api.delete_post(post_id=result.post_id)
print(delete_result.deleted_at)This repository ships a single project-local linkedin-cli skill that covers setup, auth, read/write workflows, and command selection. Its source of truth is .agents/skills/linkedin-cli/SKILL.md, and skills/, .claude/skills/, and .codex/skills/ are project-local symlinks that point at it. The same skill also installs as a Claude plugin (.claude-plugin/plugin.json). The plugin ships only the skill, so when the skill runs and linkedin-cli is missing, scripts/ensure-cli.sh installs agent-linkedin for you (requires uv or pipx installed first); then auth login → auth-status confirms read auth.
SKILL.md— skill entrypoint- initial-setup.md — first-time setup and OAuth/cookie auth
- command-cookbook.md — exact command patterns and JSON usage
- auth-troubleshooting.md — session recovery and diagnostics
- write-workflows.md — official publishing and safe mutations
uv sync --extra dev
uv run playwright install chromium
uv run ruff check .
uv run pytest -q
uv run python -m compileall linkedin_cli testsTesting rules:
- Unit tests must not require a live LinkedIn session.
- Network-sensitive behavior should sit behind transport/browser abstractions.
- Live verification is useful before releases, but it should not be required for normal CI.
- Never commit cookies, OAuth tokens, HAR files, or browser storage state.
- Never paste
LINKEDIN_COOKIE_HEADER,li_at,JSESSIONID,~/.config/linkedin/cookies.env, access tokens, client secrets, or token files into issues or pull requests. - Sanitize screenshots, logs, and terminal transcripts before sharing.
See SECURITY.md.
Read:
MIT. See LICENSE.
linkedin-cli started from frizynn/linkedin-cli by Juan Francisco Lebrero. This fork adds official LinkedIn OAuth publishing, a JSON contract layer, a Python write API, and packaged Codex/Claude skills. The original work is MIT-licensed, and its copyright is retained in LICENSE.