Skip to content
Back to skills

Post Linkedin

BSecurity

Drafts and publishes LinkedIn posts via the official API. Supports text, images, and link previews. Handles first-time OAuth setup and token refresh. Use when drafting, reviewing, or publishing LinkedIn posts.

  • 4 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added September 12, 2026
developmentpythongoshellbashreactapi

Works with

  • cli
  • api

Security analysis

B88/100
  • criticalSends environment variables or credentials to an external URL

Pro scans all 3 files and shows the line behind each finding

Scanned September 12, 2026

npx -y skills add MattVOLTA/outworkos_diy --skill post-linkedin --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Post Linkedin?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Post Linkedin
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/mattvolta-post-linkedin/badge)](https://www.skillsdirectory.com/skills/mattvolta-post-linkedin)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: post-linkedin
description: "Drafts and publishes LinkedIn posts via the official API. Supports text, images, and link previews. Handles first-time OAuth setup and token refresh. Use when drafting, reviewing, or publishing LinkedIn posts."
disable-model-invocation: true
---

# Post to LinkedIn

Draft and publish LinkedIn posts from the CLI. Always previews before publishing.

## Arguments

| Argument | Behavior |
|----------|----------|
| (none) | Interactive: ask what to post |
| `setup` | Run OAuth setup flow (first time or re-auth) |
| `<text>` | Use provided text as starting draft |

## Prerequisites

- LinkedIn Developer App with "Share on LinkedIn" product enabled
- OAuth tokens in Vault (run `/post-linkedin setup` first)
- Python 3 for image upload support

## Step 0: Check Auth State

Retrieve LinkedIn credentials from Vault via `$OUTWORKOS_ROOT/scripts/get-secret.sh`:

```bash
LINKEDIN_CLIENT_ID=$("$OUTWORKOS_ROOT/scripts/get-secret.sh" linkedin_client_id)
LINKEDIN_CLIENT_SECRET=$("$OUTWORKOS_ROOT/scripts/get-secret.sh" linkedin_client_secret)
LINKEDIN_ACCESS_TOKEN=$("$OUTWORKOS_ROOT/scripts/get-secret.sh" linkedin_access_token)
LINKEDIN_REFRESH_TOKEN=$("$OUTWORKOS_ROOT/scripts/get-secret.sh" linkedin_refresh_token)
LINKEDIN_PERSON_URN=$("$OUTWORKOS_ROOT/scripts/get-secret.sh" linkedin_person_urn)
```

Look for:
- `LINKEDIN_CLIENT_ID`
- `LINKEDIN_CLIENT_SECRET`
- `LINKEDIN_ACCESS_TOKEN`
- `LINKEDIN_REFRESH_TOKEN`
- `LINKEDIN_PERSON_URN`

If argument is `setup`, jump to **Setup Mode**.

If tokens are missing, tell the user to run `/post-linkedin setup` first and stop.

If tokens exist, test validity:

```bash
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \
  "https://api.linkedin.com/v2/userinfo"
```

- **200**: Proceed to Step 1
- **401**: Try token refresh (see reference/api-reference.md). If refresh works, store new tokens in Vault via `set-secret.sh` and proceed. If refresh fails, tell user to run `/post-linkedin setup`.

---

## Setup Mode

Guides through first-time LinkedIn app creation and OAuth flow. Follows the same pattern as `scripts/google-auth.sh`.

### S1: Detect Existing State

Check Vault for existing LinkedIn credentials (`linkedin_client_id`, `linkedin_client_secret`, `linkedin_access_token`, `linkedin_refresh_token`, `linkedin_person_urn`). Report what exists and what's missing.

### S2: App Creation (if no credentials)

Walk the user through:

1. Go to https://developer.linkedin.com/ and sign in
2. Click "Create app"
3. Settings:
   - **App name**: Any name (e.g., "OutworkOS")
   - **LinkedIn Page**: Associate with any Company Page (create a minimal one if needed)
   - **Logo**: Any image
4. After creation, go to the **Auth** tab:
   - Add redirect URL: `http://localhost:5556/callback`
   - Copy the **Client ID** and **Primary Client Secret**
5. Go to the **Products** tab:
   - Request access to **Share on LinkedIn** (auto-approved)
   - Request access to **Sign In with LinkedIn using OpenID Connect** (auto-approved)

Ask the user for their Client ID and Client Secret.

### S3: Store Credentials in Vault

Store the client credentials in Vault:

```bash
"$OUTWORKOS_ROOT/scripts/set-secret.sh" linkedin_client_id "<client-id>" "LinkedIn OAuth client ID"
"$OUTWORKOS_ROOT/scripts/set-secret.sh" linkedin_client_secret "<client-secret>" "LinkedIn OAuth client secret"
```

### S4: Run OAuth Flow

Execute the auth script:

```bash
bash "${CLAUDE_SKILL_DIR}/scripts/linkedin-auth.sh"
```

This opens a browser, completes OAuth, and saves tokens to Vault. See the script for details.

### S5: Fetch Person URN

After tokens are saved, fetch the user's LinkedIn Person URN:

```bash
LINKEDIN_ACCESS_TOKEN=$("$OUTWORKOS_ROOT/scripts/get-secret.sh" linkedin_access_token)
RESPONSE=$(curl -s -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \
  "https://api.linkedin.com/v2/userinfo")
```

Extract the `sub` field -- this is the Person URN ID. Store in Vault:

```bash
"$OUTWORKOS_ROOT/scripts/set-secret.sh" linkedin_person_urn "urn:li:person:<sub-value>" "LinkedIn Person URN"
```

### S6: Verify

Test posting capability:

```bash
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \
  -H "LinkedIn-Version: 202501" \
  -H "X-Restli-Protocol-Version: 2.0.0" \
  "https://api.linkedin.com/rest/posts?q=author&author=$LINKEDIN_PERSON_URN&count=1"
```

Note: reading posts may return 403 if `r_member_social` isn't available — that's fine. The write scope (`w_member_social`) is what matters for posting.

Report success and remind the user that tokens last 60 days (refresh tokens up to 1 year).

---

## Step 1: Discovery Interview

If the user provided ready-to-publish text as an argument, skip to Step 2.

Otherwise, **interview the user one question at a time** to gather everything needed to draft the post. Do NOT ask multiple questions at once. Wait for each answer before asking the next.

### Interview Flow

Ask these in order, adapting based on answers. Skip questions that become obvious from prior answers.

1. **"What's the core thing you want to communicate?"**
   - Could be a win, an insight, a question, an announcement, a reflection
   - Listen for the "so what" — why would someone care?

2. **"Who's the audience for this?"**
   - Their network generally? A specific industry? Potential hires? Investors? Peers?
   - This shapes tone, jargon level, and call-to-action

3. **"What's the context or backstory?"**
   - What happened that prompted this? What's the situation?
   - Pull out specific details, numbers, names — these make posts real

4. **"What do you want people to do after reading this?"**
   - React/engage? Visit a link? DM you? Apply? Share?
   - Or just "think about this differently" — that's valid too

5. **"Do you have an image, link, or document to include?"**
   - Image: provide a file path
   - Link: provide URL (will generate a preview card)
   - Neither is fine — text posts perform well on LinkedIn

6. **"Anything to avoid or specifically include?"**
   - Tone preferences, people to mention, hashtags, constraints
   - "Don't make it sound corporate" is useful input

### When You Have Enough

You have enough when you can answer: what's the message, who cares, and what's the ask. Not every question needs asking — if the user gives you a rich answer to Q1 that covers context and audience, skip ahead.

### Writing Style

When drafting, write in a natural human voice:
- No em dashes
- No "excited to announce", "thrilled to share", or other AI clichés
- No "let's dive in", "here's the thing", "game-changer"
- Short paragraphs (1-3 sentences each)
- Line breaks between paragraphs (LinkedIn renders these as visual breaks)
- Lead with a hook — the first 2 lines show before "see more"
- Hashtags: 3-5 max, at the end, only if relevant
- Mentions: use the person's name naturally in the text

---

## Step 2: Preview

Display the draft in a formatted preview:

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
LINKEDIN POST PREVIEW
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[Post text here]

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Type: text | Characters: N
Image: [path or none]
Link: [URL or none]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

Ask the user: **Publish this, edit, or cancel?**

If edit: ask what to change, revise, preview again.

---

## Step 3: Publish

### Text-Only Post

```bash
curl -s -X POST "https://api.linkedin.com/rest/posts" \
  -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \
  -H "LinkedIn-Version: 202501" \
  -H "X-Restli-Protocol-Version: 2.0.0" \
  -H "Content-Type: application/json" \
  -d @/tmp/linkedin-post.json
```

Build the JSON payload in Python (avoids shell escaping issues):

```python
import json
payload = {
    "author": "PERSON_URN",
    "commentary": "POST_TEXT",
    "visibility": "PUBLIC",
    "distribution": {
        "feedDistribution": "MAIN_FEED",
        "targetEntities": [],
        "thirdPartyDistributionChannels": []
    },
    "lifecycleState": "PUBLISHED",
    "isReshareDisabledByAuthor": False
}
with open("/tmp/linkedin-post.json", "w") as f:
    json.dump(payload, f)
```

**Success**: HTTP 201 with `x-restli-id` header containing the post URN.
**Failure**: Parse error response and report to user.

### Post with Image

See `reference/api-reference.md` for the two-step image upload flow:
1. Initialize upload → get upload URL
2. Upload binary → get image URN
3. Create post with image URN in the `content` field

### Post with Link Preview

Add an `content` block with `article` type to the payload. See `reference/api-reference.md`.

---

## Step 4: Confirm

Report:
- Post published successfully
- Post URN (for reference)
- Direct link: `https://www.linkedin.com/feed/update/{post-urn}`

---

## Token Refresh

Tokens expire after 60 days. The skill checks token validity in Step 0 and auto-refreshes when possible. If the refresh token has also expired (after ~1 year), the user must run `/post-linkedin setup` again.

See `reference/api-reference.md` for the refresh flow.

Files in this skill

  • SKILL.md9.1 KB
  • reference/api-reference.md5.6 KB
  • scripts/linkedin-auth.sh10.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.