A fast LinkedIn CLI for posting, reading, searching, and messaging via LinkedIn's Voyager API.
Inspired by bird for X/Twitter.
- Posts: Create, read, and delete posts
- Profiles: View profiles by username or URN
- Search: Search for people and companies
- Messaging: View conversations and send messages
- Agent-Friendly: JSON output mode for AI agent integration
- Cross-Platform: Works on macOS and Linux
git clone https://github.com/bvgroup-co/lnk.git
cd lnk
go build -o lnk ./cmd/lnkgo install github.com/bvgroup-co/lnk/cmd/lnk@latest# 1. Authenticate with email/password
lnk auth login -e your@email.com
# 2. Or authenticate with browser cookies
lnk auth login --browser safari
# 3. Check auth status
lnk auth status
# 4. View your profile
lnk profile me
# 5. Create a post
lnk post create "Hello LinkedIn!"
# 6. Search for people
lnk search people "software engineer"lnk auth login -e your@email.com
# You'll be prompted for your password securelyOr provide password directly:
lnk auth login -e your@email.com -p "yourpassword"lnk auth login --browser safari # macOS only
lnk auth login --browser chrome # macOS/Linux
lnk auth login --browser firefox # macOS/Linux
lnk auth login --browser brave # macOS/Linux
lnk auth login --browser arc # macOSNote: May require granting Full Disk Access to your terminal application in System Preferences > Privacy & Security.
lnk auth login --li-at "your-li_at-cookie" --jsessionid "your-jsessionid-cookie"export LNK_LI_AT="your-li_at-cookie"
export LNK_JSESSIONID="your-jsessionid-cookie"
lnk auth login --envLinkedIn requests can use an explicit proxy so cookies acquired from one network can be used from another environment through the same egress IP.
export LNK_PROXY_URL="http://user:pass@proxy.example.com:8080"
lnk profile me
lnk --proxy-url "https://proxy.example.com:8443" profile activity johndoe --json--proxy-url takes priority over LNK_PROXY_URL. If neither is set, lnk
uses Go's standard proxy environment behavior, including HTTPS_PROXY,
HTTP_PROXY, and NO_PROXY. Proxy credentials and LinkedIn cookies are
redacted from errors.
| Command | Description |
|---|---|
lnk auth login -e <email> |
Authenticate with email/password |
lnk auth login --browser <name> |
Authenticate using browser cookies |
lnk auth status |
Check authentication status |
lnk auth logout |
Clear stored credentials |
| Command | Description |
|---|---|
lnk profile me |
View your own profile |
lnk profile get <username> |
View a profile by username |
lnk profile get --urn <urn> |
View a profile by URN |
lnk profile activity <username> |
View recent profile activity |
lnk profile activity <username> --category posts |
View profile posts via Web UI GraphQL shape |
lnk profile activity <username> --category comments |
View profile comments via Web UI GraphQL shape |
lnk profile activity <username> --category reactions |
View profile reactions via Web UI GraphQL shape |
lnk profile activity <username> --category images |
View image activity |
lnk profile activity <username> --limit 20 |
Limit recent activity items |
| Command | Description |
|---|---|
lnk post create <text> |
Create a new post |
lnk post create --file post.txt |
Create post from file |
lnk post get <urn> |
Read a post by URN |
lnk post delete <urn> |
Delete a post by URN |
| Command | Description |
|---|---|
lnk search people <query> |
Search for people |
lnk search companies <query> |
Search for companies |
lnk search people <query> --limit 20 |
Limit results |
| Command | Description |
|---|---|
lnk messages list |
List conversations |
lnk messages get <conversation-urn> |
View messages in a conversation |
lnk messages send <username> <text> |
Send a message to a user |
lnk messages reply <conversation-urn> <text> |
Reply to a conversation |
Aliases: msg, dm (e.g., lnk msg list)
| Command | Description |
|---|---|
lnk feed |
Read your feed |
lnk feed --limit 20 |
Read more feed items |
lnk profile activity johndoe --category all
lnk profile activity johndoe --category posts
lnk profile activity johndoe --category comments
lnk profile activity johndoe --category reactions --json
lnk profile activity johndoe --category posts --experimental-local-filter
lnk profile activity johndoe --category comments --experimental-local-filter
lnk profile activity johndoe --category reactions --experimental-local-filter --json
lnk profile activity johndoe --category images --experimental-local-filter --json
lnk profile activity johndoe --category videos --experimental-local-filter --limit 20
lnk profile activity johndoe --category documents --experimental-local-filter
lnk profile activity johndoe --category events --experimental-local-filter
lnk profile activity johndoe --category all --debug-shape --json
lnk profile activity johndoe --limit 20
lnk profile activity johndoe --jsonThe default all category fetches LinkedIn's generic authenticated Voyager
activity feed using /feed/updatesV2?q=memberShareFeed&profileUrn=..., with a
legacy /feed/updates?profileId=...&q=memberShareFeed&moduleKey=member-share
fallback. It preserves historical behavior and is not guaranteed to match the
LinkedIn Web UI all tab.
The posts, comments, and reactions categories are supported without
experimental flags and use the Web UI GraphQL profile updates shapes:
lnk profile activity johndoe --category posts --json
lnk profile activity johndoe --category comments --json
lnk profile activity johndoe --category reactions --jsonThey call /voyager/api/graphql with category-specific
voyagerFeedDashProfileUpdates query IDs. The normalized response parser reads
the category collection *elements and resolves those references from
included. Comment and reaction detail fields are returned only when the
response structure contains them; otherwise output stays limited to stable
activity/update fields.
Recent activity JSON includes structured actor profile fields when LinkedIn
returns actor/profile objects. Post and comment actors are available under
actor; reaction actors under reactionActor. For comment activity, actor,
actorUrn, and actorName identify the selected commenter. Each structured
actor may include urn, publicIdentifier, profileUrl, firstName,
lastName, displayName, and optional avatarUrl. profileUrl is emitted
only when a URL is present in LinkedIn's payload. Existing compatibility fields
such as actorUrn, actorName, and reactionActorUrn remain populated for
older integrations.
Example recent activity actor shape:
{
"urn": "urn:li:activity:7451004062705119233",
"type": "com.linkedin.voyager.dash.feed.Update",
"actor": {
"urn": "urn:li:member:123",
"publicIdentifier": "janedoe",
"firstName": "Jane",
"lastName": "Doe",
"displayName": "Jane Doe",
"avatarUrl": "https://media.example.com/avatar.jpg"
},
"actorUrn": "urn:li:member:123",
"actorName": "Jane Doe",
"text": "hello"
}The reusable Go package also exports these types as linkedin.ActivityItem and
linkedin.ActivityActor, so clients can name structured actor fields directly.
Category compatibility is under active work:
| Category | Status | Notes |
|---|---|---|
all |
Supported generic Voyager feed | Historical behavior; not guaranteed Web UI-equivalent. |
posts |
Supported via Web UI GraphQL | feedDashProfileUpdatesByMemberShareFeed. |
comments |
Supported via Web UI GraphQL | feedDashProfileUpdatesByMemberComments. |
reactions |
Supported via Web UI GraphQL | feedDashProfileUpdatesByMemberReactions. |
videos |
Request captured, parser pending | Needs response fixture before enabling. |
| media categories | Experimental local filter only | Not Web UI-equivalent. |
Use --experimental-local-filter only when you explicitly want the legacy local
heuristic filtering for debugging. Local filters classify the generic Voyager
activity response and are not equivalent to LinkedIn Web UI category tabs.
Use --debug-shape --json with profile activity to inspect safe structural
response metadata for capture/debug work. For --category posts, comments,
and reactions, debug-shape targets the GraphQL category endpoint. Debug-shape
output includes endpoint path/query, status, top-level keys, data and included
counts, example $type values, paging keys, and next-link presence. It does not
include cookies, CSRF tokens, authorization headers, full raw responses, names,
messages, or text.
Example unsupported JSON error for categories without verified Web UI response shapes:
{
"success": false,
"error": {
"code": "UNSUPPORTED",
"message": "LinkedIn Web UI matching for category \"videos\" is not currently implemented. The previous implementation used local heuristics and may return incorrect results. Capture the Web UI request shape or retry with --experimental-local-filter if you explicitly want the legacy heuristic behavior."
}
}Captured videos request shape is documented for future parser work:
includeWebMetadata=true&variables=(contentType:VIDEOS,profileUrn:urn%3Ali%3Afsd_profile%3AREDACTED,start:0,count:20,isLookBackWindowEnabled:false,moduleKey:creator_profile_videos_content_view%3Adesktop)&queryId=voyagerFeedDashProfileContentViewModels.7719f1c8daecffaa8a087a40a775e11c
To help implement real UI-equivalent categories later, capture request shapes for the Web UI tabs without sharing secrets:
- Open LinkedIn in a browser and sign in.
- Open DevTools > Network, enable Preserve log, and filter for Fetch/XHR.
- Visit these pages for the target profile:
https://www.linkedin.com/in/USERNAME/recent-activity/all/https://www.linkedin.com/in/USERNAME/recent-activity/posts/https://www.linkedin.com/in/USERNAME/recent-activity/comments/
- For each tab, capture only the request method, path, query parameters,
non-secret header names, response status, top-level response keys, paging
keys, data/included counts, and example
$typevalues. - Do not share
Cookie,li_at,JSESSIONID,csrf-token, authorization headers, full raw responses, names, messages, post text, comments, or other private content.
Category UI URL mapping:
| Category | UI URL |
|---|---|
all |
https://www.linkedin.com/in/USERNAME/recent-activity/all/ |
posts |
https://www.linkedin.com/in/USERNAME/recent-activity/posts/ |
images |
https://www.linkedin.com/in/USERNAME/recent-activity/images/ |
videos |
https://www.linkedin.com/in/USERNAME/recent-activity/videos/ |
documents |
https://www.linkedin.com/in/USERNAME/recent-activity/documents/ |
events |
https://www.linkedin.com/in/USERNAME/recent-activity/events/ |
reactions |
https://www.linkedin.com/in/USERNAME/recent-activity/reactions/ |
comments |
https://www.linkedin.com/in/USERNAME/recent-activity/comments/ |
All commands support --json flag for structured output, making it easy to integrate with AI agents like Claude Code.
# Get profile as JSON
lnk profile me --json{
"success": true,
"data": {
"urn": "urn:li:fsd_profile:ACoAAAA",
"firstName": "John",
"lastName": "Doe",
"headline": "Software Engineer",
"profileUrl": "https://www.linkedin.com/in/johndoe"
}
}# Search for people
lnk search people "iOS developer" --json{
"success": true,
"data": [
{
"urn": "urn:li:member:123456",
"firstName": "Jane",
"lastName": "Smith",
"headline": "Senior iOS Developer",
"location": "San Francisco",
"profileUrl": "https://www.linkedin.com/in/janesmith"
}
]
}{
"success": false,
"error": {
"code": "AUTH_EXPIRED",
"message": "Session cookie expired. Run: lnk auth login"
}
}| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Authentication failure |
LinkedIn frequently changes their internal APIs. Some features may not work reliably:
| Feature | Status | Notes |
|---|---|---|
| Profile viewing | ✅ Working | |
| Post create/delete | ✅ Working | |
| Search people | ✅ Working | |
| Search companies | ✅ Working | |
| Feed | LinkedIn has restricted feed API access | |
| Messaging | LinkedIn has restricted messaging API access |
Credentials are stored in:
- macOS/Linux:
~/.config/lnk/credentials.json
You can customize the location using the XDG_CONFIG_HOME environment variable.
| Platform | Safari | Chrome | Firefox | Brave | Arc |
|---|---|---|---|---|---|
| macOS | Yes | Yes | Yes | Yes | Yes |
| Linux | No | Yes | Yes | Yes | No |
Grant Full Disk Access to your terminal:
- Open System Preferences > Privacy & Security > Full Disk Access
- Add your terminal application (Terminal, iTerm2, etc.)
LinkedIn may require captcha or 2FA verification after multiple login attempts. Solutions:
- Wait a few minutes and try again
- Use browser cookie authentication instead
- Log in via browser first, then extract cookies
Make sure you:
- Are logged into LinkedIn in the specified browser
- The browser is closed (or database is not locked)
- Have the correct permissions to read browser data
go build -o lnk ./cmd/lnkCross-platform binary targets:
GOOS=linux GOARCH=amd64 go build -o dist/lnk_linux_amd64 ./cmd/lnk
GOOS=linux GOARCH=arm64 go build -o dist/lnk_linux_arm64 ./cmd/lnk
GOOS=darwin GOARCH=amd64 go build -o dist/lnk_darwin_amd64 ./cmd/lnk
GOOS=darwin GOARCH=arm64 go build -o dist/lnk_darwin_arm64 ./cmd/lnkgo test ./...This tool uses LinkedIn's unofficial Voyager API. It is:
- Not affiliated with LinkedIn or Microsoft
- Not authorized, maintained, sponsored, or endorsed by LinkedIn
- Use at your own risk - may violate LinkedIn's Terms of Service
LinkedIn may temporarily or permanently ban accounts that use unofficial APIs. Use responsibly.
MIT License - see LICENSE for details.
- Inspired by steipete/bird
- Uses LinkedIn's internal Voyager API patterns from linkedin-api