diff --git a/AGENTS.md b/AGENTS.md index 631bae2..75e8f1a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ LinkedIn CLI built on the reverse-engineered Voyager API (Android APK). Go 1.26+ │ │ ├── profile.go # Profile endpoint │ │ └── saved.go # Saved jobs management │ ├── auth/ -│ │ └── store.go # Cookie persistence (li_at + JSESSIONID) via TOML +│ │ └── store.go # Cookie persistence (li_at + CSRF) via JSON │ ├── cmd/ # Cobra sub-command implementations │ │ ├── auth.go # lnk auth login/logout/status │ │ ├── search.go # lnk search @@ -88,7 +88,7 @@ make clean ## Key Design Decisions - **RestLi encoding** — LinkedIn's Voyager API uses a custom RestLi variable syntax. `internal/restli/encoder.go` handles this encoding. Do not replace it with plain query-string encoding; the API will reject requests. -- **Cookie auth** — Authentication uses `li_at` and `JSESSIONID` session cookies, not OAuth. Cookies are stored in a TOML file via `internal/auth/store.go`. Do not introduce OAuth flows. +- **Cookie auth** — Authentication uses `li_at` and `JSESSIONID` session cookies, not OAuth. Cookies are stored in `~/.config/lnk/credentials.json` via `internal/auth/store.go`. Do not introduce OAuth flows. - **HTML stripping** — Job descriptions from the API contain raw HTML. `internal/html/html.go` strips tags before display. Keep this layer; removing it exposes raw markup to the terminal. - **Cobra + table/JSON output** — All sub-commands use Cobra. Every command that returns data must support `--json` for machine-readable output. The `internal/output` package handles both table and JSON rendering consistently. diff --git a/README.md b/README.md index 3d6f650..1c7467d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,13 @@ Search jobs, view profiles, apply — all from your shell. Built on LinkedIn's V ![lnk demo](docs/assets/hero.png) +This uses a private, unofficial API. Automated access can violate LinkedIn's rules +and lead to account restrictions or closure. Request controls reduce accidental +load; they do not authorize automation or provide a safe threshold or ban immunity. +Read [request controls and compatibility](docs/request-compatibility.md) before use. +Applications, saved-job/alert writes, credential changes and logout require explicit +owner consent for the exact action. Discovery is not consent to apply. + ## Install **Homebrew (recommended):** @@ -155,7 +162,12 @@ lnk apply [flags] | `--dry-run` | Show what would be submitted without actually applying | | `--confirm` | Skip the confirmation prompt and apply immediately | -**Examples:** +Dry-run is explicit, not the default, and still makes quota-counted live reads. +The default prompts before submission. `--no-input` or a missing TTY without +`--confirm` fails instead of prompting. Agents must preview with `--dry-run` first; +never add `--confirm` automatically or submit without exact owner authorization. + +**Examples (submission only with exact owner consent):** ```bash lnk apply 4414623196 --dry-run # preview the submission @@ -274,7 +286,9 @@ lnk alerts delete 111222333 ### status -Show authentication state, configuration, and API connectivity. +Show authentication state, configuration, and API connectivity. With credentials, +this sends a quota-counted live Ping, then a separate `/me` read on success. +Use `lnk auth status` for local auth state or `lnk safety status` for local policy state. ``` lnk status [flags] @@ -320,8 +334,8 @@ These flags work with every command: | `--plain` | | Output as tab-separated text (pipe-friendly) | | `--no-color` | | Disable ANSI colour codes | | `--quiet` | `-q` | Suppress informational messages | -| `--verbose` | | Log HTTP requests to stderr | -| `--debug` | | Log full HTTP request/response bodies to stderr | +| `--verbose` | | Bounded HTTP method/status diagnostics on stderr | +| `--debug` | | Bounded HTTP method/status diagnostics on stderr; no bodies or URLs | | `--no-input` | | Fail instead of prompting for input | | `--config` | | Path to config file (overrides default) | @@ -330,6 +344,10 @@ These flags work with every command: **Config file:** `~/.config/lnk/config.toml` ```toml +[safety] +delay_seconds = 5 +budget = 100 + [defaults] location = "Cape Town, South Africa" sort = "relevant" # or "recent" @@ -347,25 +365,64 @@ color = true |---|---| | `LNK_LI_AT` | LinkedIn `li_at` session cookie (overrides stored credentials) | | `LNK_CONFIG` | Path to config file | -| `LNK_DEBUG` | Set to `1` to enable debug HTTP logging | +| `LNK_CSRF_TOKEN` | CSRF token after `ajax:` in JSESSIONID (overrides stored credentials) | | `NO_COLOR` | Disable ANSI colour output ([no-color.org](https://no-color.org)) | -Setting `LNK_LI_AT` lets you use `lnk` in CI/CD pipelines without writing -credentials to disk. +Both session values are required. Nonempty cookie environment values override +individual fields loaded from the credentials JSON; an unreadable or invalid file +still fails loading. `--config` overrides `LNK_CONFIG`, which overrides the default +config path; omitted TOML keys use built-ins. `LNK_DEBUG` is not supported. + +Safety settings only permit stricter limits: integer `delay_seconds >= 5` (up to +9223371916 seconds) and integer `budget` from 1 to 100. Unknown safety keys fail +with exit 2. Larger delays remain enforced even after the 24-hour admission history +expires. Config paths and cookie changes do not select a new safety namespace. ## How It Works -Reverse-engineered from the LinkedIn Android APK v4.1.1209 using standard -decompilation tools. Requests go to `https://www.linkedin.com/voyager/api/graphql` -and REST sub-paths under `/voyager/api/` using the same HTTP headers the official -Android client sends. 481 API queries extracted. Variable encoding uses the RestLi -protocol (`internal/restli`). Auth via session cookies (`li_at` + `JSESSIONID`). +The API reference was extracted from Android APK v4.1.1209. Requests use RestLi +encoding and session cookies against GraphQL and REST paths under `/voyager/api/`. +The client retains legacy Android-looking 4.1.1209/build 211700 compatibility +headers, but runs Go `net/http` using browser-session cookies. It does not establish +a native Android login. The duplicate tracking `osVersion` key is fixed; that is +header consistency, not proof of current APK identity. A session labeled “Other” +has an opaque UI classification, not a detection verdict. + +Working query IDs remain in use. Missing literals in a newer APK audit indicate +possible drift, not server removal or integration of its full catalogue. See +[compatibility limits](docs/request-compatibility.md#session-and-api-compatibility). + +## Request controls and recovery + +All LinkedIn senders share one host-user-wide in-flight request, at least five +seconds between admissions, and at most 100 admissions per rolling 24 hours by +default. Admissions include reads, writes and connectivity checks; one command can +consume several. These are operational defaults, not LinkedIn-approved thresholds. +Other hosts, apps and older binaries are outside this accounting. -Because this relies on LinkedIn's internal API: +There are no application-layer retries on refusal, transport or body errors. +The first HTTP 429 persists a cooldown of at least 30 minutes or the server's longer +`Retry-After`. HTTP 401/403/999 and known login/challenge redirects persist a manual +stop. A resource 403, expired session, temporary restriction and permanent ban are +different possibilities; the stop does not diagnose which occurred. + +```bash +lnk safety status --json +``` + +This is local and does not read credentials or contact LinkedIn. It creates policy +metadata on first use. After the owner reviews the official app/browser and +explicitly authorizes clearing a manual stop: + +```bash +lnk safety resume --confirm --json +``` -- **It may break** if LinkedIn updates or deprecates endpoints. -- **Rate-limiting is possible** — the client backs off on HTTP 429 responses. -- **LinkedIn's Terms of Service** may prohibit automated access. +Resume changes no credentials, cannot shorten cooldown or reset admission history, +and cannot clear an overflow guard or repair corrupt state. Do not delete policy +state to recover budget. State lives at `~/.local/share/lnk/safety`, independent of +`--config` and `LNK_CONFIG`. Read the [state, JSON and recovery contract](docs/request-compatibility.md) +for filesystem requirements, exit codes and remaining limits. ## Disclaimer diff --git a/docs/request-compatibility.md b/docs/request-compatibility.md index e1d1ba4..e49f6d0 100644 --- a/docs/request-compatibility.md +++ b/docs/request-compatibility.md @@ -1,5 +1,209 @@ -The LinkedIn request headers retain the legacy compatibility pair app 4.1.1209 / build 211700. `X-LI-Track` contains one `osVersion`, the SDK string `36`. These descriptive headers do not establish a native Android session or official-client equivalence. The transport is Go `net/http`. +# Request controls and session compatibility -All LinkedIn wrappers use one authenticated request boundary, which accepts only HTTPS `www.linkedin.com` Voyager API destinations and rejects redirects without following them. The external employer checker remains separate and credential-free. Injected HTTP clients are copied and receive the same redirect policy; injected transports still pass through request validation and header construction. +`lnk` uses LinkedIn's private, unofficial Voyager API. Automated access can violate +LinkedIn's rules and result in restrictions or account closure. Slower requests do +not make automation authorized. No published safe threshold, ban immunity, +human-mimic guarantee or native-session guarantee is established by these controls. +Use requires the owner's acceptance of that risk and authorization for the work. -Mutations receive one application-level send admission, without retry loops, idempotency headers or a body replay function. Go's standard transport can internally retry certain failures on reused connections, including failures before any bytes were written; an admission is not an exact packet count. Custom transports control their own internal behavior. Read retries on rate limits and timeouts remain for compatibility in M1; shared pacing, durable refusal state and retry-policy changes belong to M2. +## Admissions and configuration + +All current LinkedIn senders, including Ping and mutations, share one host-user-wide +policy. Defaults are one in-flight request, minimum five seconds between admissions, +and at most 100 admissions in the rolling interval `(now - 24h, now]`. An admission +exactly 24 hours old expires; spacing exactly at the configured delay is allowed. +Concurrent commands may interleave, but the lease covers the complete HTTP exchange, +bounded response buffering and body close. Lock acquisition waits at most 30 seconds +(exit 5). Responses larger than 16 MiB fail with exit 4 and consume their admission. +Admissions are persisted before sending, so crashes and unknown outcomes count. + +Budget units are policy admissions, not commands or exact wire packets. A command +can make several reads; `status` with credentials sends Ping then `/me` on success. +`auth status` is local credential inspection. `safety status` is local policy +inspection and does not read credentials. Compatibility fallbacks consume separate, +paced admissions. Profile typed request failures stop fallback, with a 410 exception +only at the deprecated REST profile caller, not at dash, GraphQL or later reads. + +No application-layer retry follows refusal, transport or body errors. Go +`net/http` can internally retry certain eligible requests on reused connections; +a trusted custom RoundTripper can implement its own behavior. Policy admission +cannot count every resulting wire attempt. Injected clients are copied and receive the required redirect policy; their +transports still pass through destination validation and admission. Only HTTPS +`www.linkedin.com` Voyager API destinations are accepted. Mutations have redirect following and +body replay disabled; no mutation retry/idempotency policy is added. All LinkedIn +redirects are rejected before following, including same-origin redirects. The +separate employer verifier retains its own bounded anonymous redirect rules. + +```toml +[safety] +delay_seconds = 5 +budget = 100 +``` + +These are defaults, not LinkedIn-approved thresholds. Only stricter limits are +supported: integer delay from 5 through 9223371916 seconds, integer budget from 1 +through 100. Unknown safety keys and invalid values fail with exit 2. The delay +ceiling reserves the request timeout allowance within Go's duration range. +The last admission is retained separately, so a delay longer than 24 hours remains +enforced after its timestamp leaves the rolling budget list. + +Config selection is `--config` > nonempty `LNK_CONFIG` > +`~/.config/lnk/config.toml`; omitted keys use built-ins. Credentials remain JSON at +`~/.config/lnk/credentials.json`, with `li_at`, `csrf_token`, optional `bcookie`. +Nonempty `LNK_LI_AT` and `LNK_CSRF_TOKEN` override individual loaded fields; invalid +or unreadable stored JSON still fails. Both session values are needed. `NO_COLOR` +disables color. There is no production `LNK_DEBUG` environment setting. + +## Refusal and recovery + +| Signal | Effect | +|---|---| +| First HTTP 429 | No retry; persist cooldown through max(now + 30 minutes, server Retry-After), exit 5 | +| HTTP 401, 403, 999 | Persist `block: "auth"`, exit 3 | +| Same-origin recognized auth redirect | Persist `auth`, exit 3; never follow | +| Other redirect | Reject current command, exit 4; no guessed auth latch | +| Transport/read/oversize failure | Fail current command, exit 4; admission remains counted | +| Invalid JSON/HTML where JSON expected or nonempty top-level GraphQL errors | Fail current command, generally exit 1; no body-inferred auth latch | + +Recognized redirect paths are normalized `/login`, `/uas/login`, `/authwall`, and +`/checkpoint/challenge`, including descendants and encoded variants. A 403 can be +resource-local; conservatively stopping does not identify session expiry, a temporary +restriction or a permanent ban. Unknown response text is not a verified restriction +contract. Other unsuccessful HTTP statuses normally produce general API failure. + +`Retry-After` accepts seconds or HTTP dates. Missing, invalid or past values use +the 30-minute floor. Longer representable deadlines are not capped downward. +Unrepresentable deadlines persist `retry_after_overflow`, which blocks admissions +with exit 5 and cannot be cleared by resume. + +1. Stop live commands and inspect `lnk safety status --json` locally. +2. Have the owner inspect the official app/browser and decide whether to continue. +3. Only on explicit owner instruction after review, clear the manual auth stop: + +```sh +lnk safety resume --confirm --json +``` + +This command has no network or credential effect. It clears `auth`, preserves +cooldown and unexpired admission history, and returns the same status schema. +It can succeed during cooldown: that does not authorize a new admission yet. +It cannot repair corrupt/unsafe state, fix clock rollback, clear overflow or reset +budget. Do not delete policy state or its lock/initialization files as routine +recovery. No automatic login, credential refresh, CAPTCHA solving or session +revocation is part of recovery. + +## Local state and limits + +The fixed namespace is `~/.local/share/lnk/safety`, containing `lock`, `initialized` +and `state.json`. It is independent of `--config`, `LNK_CONFIG`, cookie values and +account identity; different accounts on one host user intentionally share it. +Other hosts, browsers, apps and older binaries are not counted. It has no distributed +coordination or automatic background polling. Deliberate deletion of the entire +namespace by the same user is outside the accidental-load threat model. + +Status is local but not read-only: it acquires the lease, initializes metadata on +first use, advances the greatest observed UTC wall clock, prunes expired admissions +and saves state. Resume does the same before clearing an eligible stop. Neither +consumes an admission. Successful status can report a block or active cooldown. + +State contains version, admission timestamps, last admission, greatest observed +clock, cooldown and enumerated block; no cookie values, account identifiers or +profile payloads. Successful locked observations prune admissions at least 24 hours +old. Last admission/clock and refusal metadata remain; an elapsed cooldown timestamp +can remain visible even though it no longer blocks. + +Only absence of both marker and state permits initialization. Missing one, corrupt +or unreadable state, unexpected ownership, symlinks, nonregular/hard-linked files, +unsafe ancestor permissions, or invalid modes fail closed. The private directory +requires exactly 0700, files 0600 and no extended ACL entries. A restrictive umask +removing required owner bits can prevent initialization; existing permissions are +not silently repaired. Writes are atomic in the same directory and sync files and +directory. The OS releases advisory locks; the lock file is never unlinked. + +Supported filesystem checks are local APFS/HFS on macOS and the ext-family, XFS, +Btrfs, tmpfs and overlayfs type allowlist on Linux, with working ACL inspection, +locking and sync. Unsupported filesystems/ACL semantics fail closed. These checks +are not a guarantee for every mount configuration. If the clock moves below the +greatest saved observation, state operations fail with exit 4 until time catches up +or the owner reviews it; time is not clamped to release budget. + +## JSON and exits + +Both `lnk safety status --json` and `lnk --json safety status` work. Resume likewise +accepts `--json` before the group or after the leaf. Successful status/resume JSON +goes to stdout with exactly these fields. This is a fresh synthetic local fixture, +not a live account response: + +```json +{"admissions":0,"budget":100,"delay_seconds":5,"cooldown":"0001-01-01T00:00:00Z","block":""} +``` + +`admissions` is the current rolling count; `budget` and `delay_seconds` are the +selected config. `cooldown` is a UTC timestamp (the zero timestamp means unset). +`block` is empty, `auth` or `retry_after_overflow`. + +| Exit | Meaning | +|---|---| +| 0 | Command success; safety status may still report blocked state | +| 1 | General command/API/body error | +| 2 | Usage, config, invalid flags or missing resume confirmation | +| 3 | Authentication/refusal/manual-review stop | +| 4 | Transport, response read/size, state I/O/permissions or clock failure | +| 5 | Budget, cooldown, Retry-After overflow or lock timeout | + +Root diagnostic JSON goes to stderr, leaving stdout empty on these safety failures. +It always contains `error` (bounded generic message), `code` and `reason`. +Optional request fields are `method` (GET/POST/DELETE), `operation` +(read/graphql/normalized_read/post/delete) and numeric `status` (100..999), only +when available. `reason` is one of `http_status`, `redirect`, `transport`, +`response_read`, `response_size`, `invalid_json`, `graphql_errors`, `refusal`, or +fallback `usage`, `auth_stop`, `network_state`, `rate_limit`, `general`. +The status is diagnostic, not a new exit code. Missing resume confirmation yields: + +```json +{"code":2,"error":"invalid arguments or configuration","reason":"usage"} +``` + +`--verbose` and `--debug` show bounded HTTP method/status on stderr. They never +unlock full bodies, headers, URLs or cookies. Explicit job/profile JSON still +contains the requested data; this is distinct from diagnostics. + +## Session and API compatibility + +The reference extraction used Android APK 4.1.1209. Current headers retain legacy +app version 4.1.1209/build 211700, Android-looking model/OS and Cronet text as +compatibility metadata. The actual transport is Go `net/http`, and authentication +reuses browser-session cookies. Typed tracking JSON fixes the duplicate `osVersion` +key (SDK string `36`); it does not prove a native Android login, current APK identity +or server acceptance of every header claim. + +The reported “Other” session label is an opaque UI classification. It establishes +neither detection nor successful Android impersonation. Its cause is unknown here. +Only genuine official Android session evidence can establish an Android login; +even that does not authorize a third-party client. No attestation/device ID/TLS +spoofing, proxy rotation or new login to alter a label is proposed. + +The separate audit observed mirror APK versions 4.1.1259 and beta 4.1.1261, without +proving Play rollout or publisher-signature authenticity. Six CLI query IDs absent +from beta DEX literals are drift evidence, not proof of server removal. Existing +working query IDs remain; the full newer catalogue has not been integrated. Any +replacement needs query-name, variables and response-schema evidence and separately +authorized bounded live verification. M3 help and offline fixtures prove local +behavior only, not current LinkedIn compatibility or native-session classification. + +## Applications and agent permissions + +The [CLI skill](../skill/SKILL.md#apply) and [README](../README.md#job) retain the +accepting/closed/unverified evidence contract and the isolated Lever/Workday verifier. +`job --check-apply` can contact a supported employer anonymously; its traffic is +separate from LinkedIn admission accounting. HTTP 200 or generic Apply text is not +application proof. Check deadlines and suitability separately. + +`apply` prompts by default; `--dry-run` defaults false. Explicit dry-run previews +without submitting but still makes quota-counted live reads. `--confirm` skips the +prompt; `--no-input` or missing TTY without confirmation fails instead of prompting. +Agents must preview first and never add `--confirm` automatically. Only exact owner +authorization permits an application, saved-job/alert mutation or credential logout. +Local resume also requires explicit owner instruction after official-session review. +Bounded unattended reads require authorization and retain account risk; they are +not permission for unlimited polling. Never automatically obtain browser cookies. diff --git a/skill/SKILL.md b/skill/SKILL.md index 130b4ab..76ee99f 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -1,6 +1,6 @@ --- name: linkedin -description: LinkedIn from the terminal via the lnk CLI. Search and view jobs, apply via Easy Apply, view profiles, manage saved jobs and job alerts. +description: Use lnk for LinkedIn jobs, profiles and account actions. --- # linkedin @@ -12,8 +12,9 @@ job feed, and manage saved jobs and job-alert subscriptions — all from the shell. Every data command supports `--json` for machine-readable output. > Unofficial tool, not affiliated with LinkedIn. It talks to LinkedIn's private -> API, so it may break if endpoints change, can be rate-limited (backs off on -> HTTP 429), and automated access may violate LinkedIn's Terms of Service. +> API, so endpoints can break and automated access can violate LinkedIn's rules +> and lead to restrictions or account closure. Request pacing is load control, +> not authorization, a safe threshold, human mimicry or ban immunity. ## Install @@ -44,25 +45,96 @@ must obtain the cookies out of band from a real logged-in session. | Command | Description | |---|---| | `lnk auth login` | Interactively prompt for `li_at`, `JSESSIONID` (`ajax:`), and optional `bcookie`; save them | -| `lnk auth status` | Show current authentication state | +| `lnk auth status` | Show local authentication state; no network | | `lnk auth logout` | Remove stored credentials | -| `lnk status` | Show auth state, config path, and live API connectivity | +| `lnk status` | With credentials, quota-counted live Ping then `/me`; may consume two admissions | **Where credentials live:** `~/.config/lnk/credentials.json`, written atomically with `0600` permissions (directory `0700`). JSON fields: `li_at`, `csrf_token`, `bcookie`. **Environment-variable overrides** (take precedence over the file — useful for -CI or headless use without writing cookies to disk): +explicitly authorized headless use without writing cookies to disk): | Variable | Description | |---|---| | `LNK_LI_AT` | `li_at` session cookie | | `LNK_CSRF_TOKEN` | CSRF token (the value after `ajax:` from `JSESSIONID`) | | `LNK_CONFIG` | Path to config file | -| `LNK_DEBUG` | `1` to enable debug HTTP logging | | `NO_COLOR` | Disable ANSI colour | +Nonempty cookie overrides apply per field after file loading; invalid/unreadable +stored JSON still fails. Config precedence is `--config`, then `LNK_CONFIG`, then +`~/.config/lnk/config.toml`; missing keys use defaults. `LNK_DEBUG` is unsupported. +Never print cookies or extract them from a browser/keychain automatically. Login +and credential replacement require owner consent and out-of-band cookie entry. + +## Request controls and recovery + +Default policy: minimum five seconds between admissions, maximum 100 admissions +per rolling 24 hours, one host-user-wide in-flight LinkedIn request. Reads, writes, +Ping and compatibility fallbacks all consume admissions; commands may use several. +Deduplicate and bound reads; do not set up unlimited polling. Other machines, apps +and older binaries are not counted. Admissions do not measure exact wire packets: +Go transport internal retries or trusted custom transports are a limitation. + +Only stricter TOML settings are supported: + +```toml +[safety] +delay_seconds = 5 +budget = 100 +``` + +Delay must be an integer from 5 through 9223371916 seconds; budget an integer from +1 through 100. Unknown safety keys are invalid (exit 2). Delays beyond 24 hours +still use the retained last admission after rolling history expires. + +No application-layer retry follows refusal, transport or body failure. First 429 +persists max(30 minutes, server `Retry-After`); 401/403/999 and recognized same-origin +login/authwall/challenge redirects latch manual review. A resource 403 does not +prove session expiry or permanent ban. Unknown body errors fail the command without +guessing an auth latch. Profile compatibility fallbacks are separate paced +admissions; typed request failures stop them, except 410 at the deprecated REST +profile caller. + +`lnk safety status --json` is local, reads no credentials, and initializes metadata +on first use. JSON stdout fields are `admissions`, `budget`, `delay_seconds`, +`cooldown` (UTC timestamp, zero `0001-01-01T00:00:00Z`), and `block` (empty, +`auth`, or `retry_after_overflow`). It can return success while blocked; inspect +the fields. Both `lnk --json safety status` and the trailing flag work. + +Only after official app/browser review and the owner's explicit instruction, run +`lnk safety resume --confirm --json`. It clears the manual auth stop locally, +without network or credential changes. It preserves cooldown and unexpired history, +cannot clear the overflow guard or repair unsafe/corrupt state. Never automatically +resume, delete policy state, reset budget, refresh credentials or solve challenges. + +State is fixed at `~/.local/share/lnk/safety`, independent of config path and cookies. +It records timestamps and enumerated stop metadata, not credentials or profile data. +Locked operations prune admissions at least 24 hours old, retaining last admission, +greatest observed clock and refusal metadata. Clock rollback and unsafe state fail +closed. Owner-only modes (directory 0700, files 0600), no extended ACLs/symlinks, +supported local filesystem and locking/sync semantics are required. Restrictive +umasks that remove required owner bits can fail closed; never weaken validation. + +Success is exit 0; general/body/API errors 1; usage/config/missing resume confirmation +2; auth/refusal stop 3; transport/state/clock failure 4; budget/cooldown/overflow/lock +timeout 5. Root `--json` errors go to stderr with `error`, `code`, `reason` and +optional allowlisted `method`, `operation`, numeric `status`. Successful data goes +to stdout. Verbose/debug diagnostics contain bounded method/status only, no full +URLs, credentials or bodies. Explicit data JSON still contains requested data. + +Legacy Android-looking 4.1.1209/build 211700 headers are compatibility metadata; +Go transport and browser cookies do not prove native Android identity. The tracking +duplicate is fixed, not the full current APK catalogue integrated. “Other” is an +opaque session label, not a detection verdict. Keep working query IDs: absent newer +APK literals are drift evidence, not removal proof. Never spoof attestation, device +IDs or TLS, rotate proxies, or create logins to change the label. + +For filesystem details, Retry-After overflow and diagnostics, read the repository's +[request compatibility reference](../docs/request-compatibility.md). + ## Search & jobs | Command | Description | @@ -137,7 +209,9 @@ submitted (your profile data), asks for confirmation, then submits. A known-clos job is rejected. If Easy Apply is unavailable, an observed employer URL is labeled unverified, or the command states that no employer URL was observed. It never labels the generated LinkedIn listing URL as an employer URL. `--dry-run` previews -without submitting; `--confirm` skips the prompt. +without submitting, but still makes quota-counted live reads; it is not the default. +`--confirm` skips the prompt. `--no-input` or missing TTY without `--confirm` +fails instead of prompting. Never supply `--confirm` automatically. ## Saved jobs @@ -165,15 +239,16 @@ without submitting; `--confirm` skips the prompt. ## Global flags Work on every command: `--json` (machine-readable), `--plain` (tab-separated, -pipe-friendly), `--no-color`, `--quiet`/`-q`, `--verbose` (log HTTP requests to -stderr), `--debug` (full request/response bodies), `--no-input` (fail instead of +pipe-friendly), `--no-color`, `--quiet`/`-q`, `--verbose` and `--debug` (bounded HTTP method/status diagnostics on +stderr, without URLs or bodies), `--no-input` (fail instead of prompting), `--config `. ## Headless / agent usage -**Read commands are safe to run unattended once credentials exist:** `search`, +**Owner-authorized bounded reads can run without prompts once credentials exist:** `search`, `job` (without `--open`, including `--check-apply`), `feed`, `profile`, `saved list`, `alerts list`, -`status`, `auth status`. Add `--json` (or `--plain`) to parse output. Avoid +`status`, `auth status`, `safety status`. This does not grant account access or +permission for unlimited polling and does not remove account risk. Add `--json` (or `--plain`) to parse output. Avoid `job --open`, which launches a browser. **Auth headless:** `lnk` needs the `li_at` + CSRF cookies from an existing @@ -193,8 +268,7 @@ the cookie values come from the user's browser, out of band. Pass `--no-input` to any command to make it fail loudly instead of blocking on a prompt when credentials are missing. -**Write / action commands — treat as confirm-gated, run only when the user -explicitly asks:** +**Write / action commands require exact owner consent; not all have CLI prompts:** - `lnk apply ` — submits a real job application. It prompts for confirmation by default; **preview first with `--dry-run`.** `--confirm` @@ -202,6 +276,7 @@ explicitly asks:** - `lnk saved add` / `lnk saved remove` — modify the user's saved-jobs list. - `lnk alerts create` / `lnk alerts delete` — modify job-alert subscriptions. - `lnk auth logout` — deletes stored credentials. +- `lnk safety resume --confirm` — clears a manual policy stop after owner review. Never run an application, saved-job, or alert write without explicit consent for that exact action. Discovery is not consent.