Django-free library and CLI for LinkedIn platform mechanics over a bound browser session (Voyager API + Playwright).
Project description
linkedin-cli
Drive LinkedIn from the command line or any program — search people, scrape profiles, check connection status, send connection requests, and read or send messages. One small, dependency-light Python tool that talks to LinkedIn's private Voyager API through a real, logged-in browser (Playwright), so it behaves like a human session instead of a cookie-only scraper.
No SaaS, no API key, no database. Your browser, your LinkedIn account, your machine.
✨ Why linkedin-cli
- Real browser session, not raw cookies. A persistent Chromium window is launched once and shared; requests ride your live, authenticated session — far more resilient than header/cookie replay.
- Structured JSON out of every command. Pipe it into
jq, a script, or an LLM agent. Human-readable summaries by default;--jsonfor the full record. - Robust login. Authentication is a small page-state machine that understands LinkedIn's login, authwall, and security-checkpoint redirects — not a brittle one-shot form fill.
- Language-agnostic. Anything that can run a subprocess and parse JSON can drive LinkedIn — Python, Node, Go, shell, or an AI agent. No SDK lock-in.
- Tiny surface. Eight verbs, four dependencies, zero web framework. It knows about a LinkedIn page and a browser — nothing else.
📦 Install
pip install linkedin-agent-cli
python -m playwright install chromium
This installs the linkedin-cli command (equivalent to python -m linkedin_cli.cli).
The PyPI package is linkedin-agent-cli; the import name is linkedin_cli. For the
latest unreleased code, install from git:
pip install "linkedin-agent-cli @ git+https://github.com/eracle/linkedin-cli.git@main".
🚀 Quickstart
linkedin-cli uses a bind + connect model: one long-lived process owns the browser; every command is a short client that connects to it.
# 1. Open + bind a session once (this process owns the browser window).
linkedin-cli session open --session work
# 2. From any other shell, drive it. Set the session once via env:
export LINKEDIN_CLI_SESSION=work
export LINKEDIN_USERNAME="you@example.com"
export LINKEDIN_PASSWORD="••••••••"
linkedin-cli login # authenticate the session
linkedin-cli search "head of growth" --network first # discover → handles
linkedin-cli profile alice-smith # scrape a profile
linkedin-cli profile alice-smith --json > alice.json # save the full record
linkedin-cli status alice-smith # Connected / Pending / Qualified
linkedin-cli connect alice-smith # send a connection request
linkedin-cli message alice-smith --text "Hi Alice 👋"
linkedin-cli thread alice-smith # read the conversation
linkedin-cli session close
Hit a security checkpoint? playwright-cli attach work opens the same browser
so you can clear it by hand, then carry on.
🧰 Commands
--session <name> (or $LINKEDIN_CLI_SESSION) and --json apply to every command.
| Command | What it does | --json result |
|---|---|---|
login |
Authenticate the session (creds from env), clear checkpoints, discover your own profile | {account, self} |
whoami |
Who is this session logged in as (no login flow) | {self} |
search <kw> [--network first/second/third] [--page N] |
People search → matching profile handles | {query, page, network, profiles[]} |
profile <id> |
Scrape a profile (positions, education, location, …); --raw adds the raw Voyager blob |
full LinkedInProfile |
status <id> |
Connection state | {public_identifier, state} |
connect <id> |
Send a connection request (no note) | {public_identifier, state} |
message <id> --text … |
Send a direct message | {public_identifier, sent} |
thread <id> |
Read a conversation's messages | {public_identifier, messages[]} |
An <id> is a public handle (alice-smith) or a full profile URL. Commands that
need the internal member urn (message/thread/status) resolve it for you —
every command is independent and takes only a handle.
🤖 Built for AI agents (and any language)
linkedin-cli is designed to be driven by an LLM as a deterministic tool. The properties that make it agent-friendly:
- Stable, typed JSON contract — every verb returns one documented dict; maps directly onto a function-calling / tool-use schema.
- id-only, stateless commands — a public handle is the only argument an agent threads between steps; no session tokens, urns, or cursors to carry.
- Predictable error taxonomy — failures surface as
error: <type>: <message>on stderr with a non-zero exit, so an agent can branch ontype(checkpoint_challenge,authentication,connection_limit, …). - No hidden state or side effects — stdout is result-only; logs go to stderr.
- Self-describing — see
llms.txtfor a compact spec an LLM can load directly, andlinkedin-cli <verb> --helpfor per-verb usage.
Because every command emits JSON on stdout, you can drive LinkedIn from anything — Python, Node, Go, shell, or an agent loop — no SDK and no Python import required:
import subprocess, json
def li(*args):
out = subprocess.run(["linkedin-cli", *args, "--json"],
capture_output=True, text=True, check=True)
return json.loads(out.stdout)
for hit in li("search", "head of growth", "--network", "first")["profiles"]:
handle = hit["public_identifier"]
if li("status", handle)["state"] == "Qualified":
li("message", handle, "--text", "Hi — loved your recent post!")
The discovery → outreach loop an agent runs: search → profile / status →
message / thread.
🧠 How it works
- bind + connect —
linkedin-cli session openlaunches a persistent Chromium withBrowser.bind()(Playwright ≥ 1.59) and registers a localws://endpoint under the session name. Each commandchromium.connect()s to that same browser and drives a real page. Auth, cookies, and fingerprint live in the owner's on-disk profile; the CLI keeps only a name→endpoint registry — no database. One session = one LinkedIn account. - Voyager API — reads (
profile,thread,status) call LinkedIn's private Voyager endpoints from inside the authenticated page (fetch), then parse the JSON — fast and structured, no DOM scraping where an API exists. - Page-state auth machine —
classify_page()judges the live page by URL path only (so a/login?...redirect=/feed/URL never reads as the feed), and each transition asserts its pre/post state, raising on an illegal jump. Login, authwall, and checkpoint flows are modeled explicitly.
📤 Output contract
- Every command produces one result dict — that dict is both the
--jsonpayload and the source the human summary is rendered from, so the two never drift. - Human-readable by default;
--jsonfor the full dict. - No
--outflag — print to stdout, redirect to save (… --json > out.json). - stdout is result-only; logs and errors go to stderr as
error: <type>: <message>with a non-zero exit. Error types are stable:checkpoint_challenge,authentication,profile_inaccessible,skip_profile,connection_limit.
⚠️ Responsible use
This tool automates your own LinkedIn account from your own machine. Automating LinkedIn may conflict with its Terms of Service, and aggressive use can get an account restricted. Respect rate limits, only contact people for legitimate reasons, follow applicable laws (GDPR/CAN-SPAM), and use it at your own risk. You are responsible for how you use it.
📄 License
MIT © eracle
linkedin-cli was extracted from OpenOutreach, an open-source AI outreach tool, where it powers the LinkedIn discovery and interaction layer. It is fully standalone and reusable on its own.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file linkedin_agent_cli-0.1.0.tar.gz.
File metadata
- Download URL: linkedin_agent_cli-0.1.0.tar.gz
- Upload date:
- Size: 34.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f1d637553e685fcb81df5fa20d76337fda8b8430e6770979607881d06779662
|
|
| MD5 |
048ab11c3d4c6f40eac91775d0169ca4
|
|
| BLAKE2b-256 |
2e73b5f7905497a64c5c2c50e873805263d3ea03c01428e2fbe4395298aea51d
|
File details
Details for the file linkedin_agent_cli-0.1.0-py3-none-any.whl.
File metadata
- Download URL: linkedin_agent_cli-0.1.0-py3-none-any.whl
- Upload date:
- Size: 45.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9a21533ebbb6d1ffe3320313199bbb9041f969ac22af0808a293d12a098f18c6
|
|
| MD5 |
f7f60253f64db1029a4c33ba9846c4ce
|
|
| BLAKE2b-256 |
1d61c0e587b579381935bb73e6cccfc9b2d3112bc713f95411304cbded8ac35f
|