Summary
Add multi-profile support so users working across multiple Linear workspaces can store multiple API tokens and switch between them with --profile <name>. Profiles are simply different ~/.linear_api_token_* files — no config files, no TOML, no new dependencies. lineark stays config-free.
Inspired by #115 (thanks @lightstrike for the idea and effort). This issue captures the design we want to implement instead — same goal, different approach that preserves lineark's zero-config philosophy.
Design Decisions
1. Profile = named token file
Profiles are just ~/.linear_api_token_{name} files. No config file format, no parsing, no new dependencies.
| Profile |
Token file |
| (default) |
~/.linear_api_token |
work |
~/.linear_api_token_work |
banana |
~/.linear_api_token_banana |
Rationale: lineark's design principle is "zero config for existing Linear users." A single token file already works today — multiple profiles are just more files following the same pattern. This avoids introducing TOML parsing, XDG config directories, or any new dependency.
2. --profile flag on CLI
lineark --profile work teams list # uses ~/.linear_api_token_work
lineark teams list # uses default auth chain (env → ~/.linear_api_token)
--profile is a global flag (#[arg(long, global = true)]), same as --api-token and --format.
3. Auth precedence — --profile is an intentional override
When --profile is specified, it skips the env var and goes straight to the named file. The user explicitly asked for a specific profile — the env var should not silently win.
Full precedence chain:
With --profile:
1. --api-token flag (but conflicts_with --profile, so this is a hard error)
2. ~/.linear_api_token_{profile} file
Without --profile:
1. --api-token flag
2. $LINEAR_API_TOKEN env var
3. ~/.linear_api_token file
Rationale: --profile work means "I want the work token." If $LINEAR_API_TOKEN is also set, the user's intent is clear — they want the profile, not the env var. Letting the env var win would be surprising.
4. --api-token + --profile = hard error
These flags are mutually exclusive via clap's conflicts_with. If both are provided, clap errors before the program runs.
Rationale: Silent precedence creates confusion. If someone passes both, it's a mistake — better to catch it immediately. The codebase already uses conflicts_with in other commands (cycles.rs, labels.rs).
5. SDK becomes path-agnostic (breaking change)
The SDK currently bakes in ~/.linear_api_token via Client::auto() and Client::from_file(). This is a filesystem convention that belongs in the CLI, not the SDK. Library consumers should be able to read tokens from any path.
Remove from SDK:
Client::auto() — bakes in path convention
Client::from_file() — bakes in ~/.linear_api_token
auth::auto_token() — combines env + hardcoded path
auth::token_from_file() (no-arg version) — hardcoded path
Keep/add in SDK:
Client::from_token(token: impl Into<String>) — unchanged
Client::from_env() — reads $LINEAR_API_TOKEN, unchanged
Client::from_token_file(path: &Path) — new, reads token from any file path (read, trim, good error messages with path included)
auth::token_from_env() — unchanged
auth::token_from_file(path: &Path) — changed signature, accepts any path instead of hardcoding ~/.linear_api_token
Rationale: The SDK is a library. Library consumers may store tokens wherever they want. The ~/.linear_api_token convention is a CLI UX decision, not a library concern. This also cleanly separates profile logic: the CLI owns the naming convention, the SDK just reads files.
This is a breaking change. Current version → next major version.
6. CLI owns the convention
The CLI's client resolution in main.rs becomes:
let home = home::home_dir().expect("could not determine home directory");
let client = match (&cli.api_token, &cli.profile) {
(Some(_), Some(_)) => unreachable!(), // clap conflicts_with prevents this
(Some(token), None) => Client::from_token(token),
(None, Some(profile)) => {
Client::from_token_file(&home.join(format!(".linear_api_token_{profile}")))
}
(None, None) => {
Client::from_env()
.or_else(|_| Client::from_token_file(&home.join(".linear_api_token")))
}
};
7. Profile discovery in lineark usage
lineark usage currently shows hints like (set) / (found) for env var and token file. Update it to discover and list available profiles.
Discovery: glob ~/.linear_api_token_* (note the underscore — this avoids matching backup files like .linear_api_token.bak). Parse the suffix after _ as the profile name.
Filter: strip test from the displayed list. The _test file is for CI/online tests and would confuse users.
Output example:
AUTH (in precedence order):
1. --api-token flag
2. $LINEAR_API_TOKEN env var (set)
3. ~/.linear_api_token file (found)
found profiles: "default", "work", "banana". switch with --profile <name>
"default" appears when ~/.linear_api_token exists (no suffix = default profile).
8. Error UX when profile file is missing
When --profile work is specified but ~/.linear_api_token_work doesn't exist:
Error: Profile "work" not found. Available profiles: "default", "banana".
Create it with:
echo "lin_api_..." > ~/.linear_api_token_work
If no profiles exist at all:
Error: Profile "work" not found. No profiles found.
Create it with:
echo "lin_api_..." > ~/.linear_api_token_work
Rationale: Actionable error messages. The user knows exactly what to do and can see what's available.
9. Refactor test-utils to use SDK
lineark-test-utils currently hardcodes ~/.linear_api_token_test with its own file-reading logic. Refactor to use the SDK's token_from_file():
// Before (hardcoded path + own file reading)
pub fn test_token() -> String {
let path = home::home_dir().unwrap().join(".linear_api_token_test");
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("could not read {}: {}", path.display(), e))
.trim()
.to_string()
}
// After (uses SDK)
pub fn test_token() -> String {
let path = home::home_dir().unwrap().join(".linear_api_token_test");
lineark_sdk::auth::token_from_file(&path)
.unwrap_or_else(|e| panic!("could not read test token: {}", e))
}
Rationale: Single source of truth for reading token files. The SDK owns file reading (read, trim, error messages), consumers just provide a path.
10. No LINEAR_PROFILE env var
The PR proposed LINEAR_PROFILE env var support. We're not doing this. Profile is not a secret — it doesn't need the env var treatment that tokens get (avoiding ps aux leaks). The --profile flag is sufficient.
Implementation Plan
Step 1: SDK changes (crates/lineark-sdk/)
src/auth.rs:
- Change
token_from_file() signature to token_from_file(path: &Path) -> Result<String, LinearError>
- Remove
auto_token() (it combined env + hardcoded path)
- Remove
token_file_path() helper (hardcoded ~/.linear_api_token)
- Keep
token_from_env() unchanged
- Update unit tests — remove tests for removed functions, update tests for new
token_from_file(&Path) signature
src/client.rs:
- Remove
Client::auto()
- Remove
Client::from_file()
- Add
Client::from_token_file(path: &Path) -> Result<Self, LinearError>
- Keep
Client::from_token() and Client::from_env() unchanged
src/lib.rs:
- Update re-exports if needed
src/helpers.rs:
- Update doc examples that reference
Client::auto()
README.md:
- Update auth documentation table — remove
auto row, add from_token_file row
Step 2: CLI changes (crates/lineark/)
src/main.rs:
- Add
--profile flag to Cli struct with conflicts_with = "api_token"
- Update client resolution to the three-branch match (api_token / profile / default)
- Add profile discovery helper (glob + filter) for error messages
src/commands/usage.rs:
- Add profile discovery: glob
~/.linear_api_token_*, strip test, display found profiles
- Update AUTH section to mention
--profile
README.md:
- Add multi-profile section with setup examples
Step 3: test-utils refactor (crates/lineark-test-utils/)
src/token.rs:
- Replace hardcoded file reading with
lineark_sdk::auth::token_from_file(&path)
- Keep
test_token() and no_online_test_token() public API unchanged
Step 4: Tests
SDK unit tests (crates/lineark-sdk/src/auth.rs):
token_from_file with valid file (tempfile)
token_from_file with missing file (good error message with path)
token_from_file with empty file
token_from_file trims whitespace
token_from_env unchanged tests
CLI offline tests (crates/lineark/tests/offline.rs):
--profile work with valid ~/.linear_api_token_work (use tempdir + HOME override)
--profile work with missing file → error message includes profile name, available profiles, creation hint
--profile + --api-token → clap error
--help shows --profile flag
lineark usage shows discovered profiles
- Default (no flag) still works with
~/.linear_api_token
- Default (no flag) still works with
$LINEAR_API_TOKEN env var
Step 5: Documentation
- Update
lineark usage output
- Update CLI README
- Update SDK README
- Run
/update-docs before merging
Out of Scope
LINEAR_PROFILE env var — not needed, --profile flag suffices
- TOML config file — lineark stays config-free
toml dependency — not needed
- Profile switching within a session — one profile per invocation
- SDK-level profile awareness — SDK is path-agnostic, CLI owns conventions
Version Impact
This is a breaking change to the SDK's public API (Client::auto() and Client::from_file() removed). Bump to next major version.
References
Summary
Add multi-profile support so users working across multiple Linear workspaces can store multiple API tokens and switch between them with
--profile <name>. Profiles are simply different~/.linear_api_token_*files — no config files, no TOML, no new dependencies. lineark stays config-free.Inspired by #115 (thanks @lightstrike for the idea and effort). This issue captures the design we want to implement instead — same goal, different approach that preserves lineark's zero-config philosophy.
Design Decisions
1. Profile = named token file
Profiles are just
~/.linear_api_token_{name}files. No config file format, no parsing, no new dependencies.~/.linear_api_tokenwork~/.linear_api_token_workbanana~/.linear_api_token_bananaRationale: lineark's design principle is "zero config for existing Linear users." A single token file already works today — multiple profiles are just more files following the same pattern. This avoids introducing TOML parsing, XDG config directories, or any new dependency.
2.
--profileflag on CLI--profileis a global flag (#[arg(long, global = true)]), same as--api-tokenand--format.3. Auth precedence —
--profileis an intentional overrideWhen
--profileis specified, it skips the env var and goes straight to the named file. The user explicitly asked for a specific profile — the env var should not silently win.Full precedence chain:
Rationale:
--profile workmeans "I want the work token." If$LINEAR_API_TOKENis also set, the user's intent is clear — they want the profile, not the env var. Letting the env var win would be surprising.4.
--api-token+--profile= hard errorThese flags are mutually exclusive via clap's
conflicts_with. If both are provided, clap errors before the program runs.Rationale: Silent precedence creates confusion. If someone passes both, it's a mistake — better to catch it immediately. The codebase already uses
conflicts_within other commands (cycles.rs, labels.rs).5. SDK becomes path-agnostic (breaking change)
The SDK currently bakes in
~/.linear_api_tokenviaClient::auto()andClient::from_file(). This is a filesystem convention that belongs in the CLI, not the SDK. Library consumers should be able to read tokens from any path.Remove from SDK:
Client::auto()— bakes in path conventionClient::from_file()— bakes in~/.linear_api_tokenauth::auto_token()— combines env + hardcoded pathauth::token_from_file()(no-arg version) — hardcoded pathKeep/add in SDK:
Client::from_token(token: impl Into<String>)— unchangedClient::from_env()— reads$LINEAR_API_TOKEN, unchangedClient::from_token_file(path: &Path)— new, reads token from any file path (read, trim, good error messages with path included)auth::token_from_env()— unchangedauth::token_from_file(path: &Path)— changed signature, accepts any path instead of hardcoding~/.linear_api_tokenRationale: The SDK is a library. Library consumers may store tokens wherever they want. The
~/.linear_api_tokenconvention is a CLI UX decision, not a library concern. This also cleanly separates profile logic: the CLI owns the naming convention, the SDK just reads files.This is a breaking change. Current version → next major version.
6. CLI owns the convention
The CLI's client resolution in
main.rsbecomes:7. Profile discovery in
lineark usagelineark usagecurrently shows hints like(set)/(found)for env var and token file. Update it to discover and list available profiles.Discovery: glob
~/.linear_api_token_*(note the underscore — this avoids matching backup files like.linear_api_token.bak). Parse the suffix after_as the profile name.Filter: strip
testfrom the displayed list. The_testfile is for CI/online tests and would confuse users.Output example:
"default" appears when
~/.linear_api_tokenexists (no suffix = default profile).8. Error UX when profile file is missing
When
--profile workis specified but~/.linear_api_token_workdoesn't exist:If no profiles exist at all:
Rationale: Actionable error messages. The user knows exactly what to do and can see what's available.
9. Refactor test-utils to use SDK
lineark-test-utilscurrently hardcodes~/.linear_api_token_testwith its own file-reading logic. Refactor to use the SDK'stoken_from_file():Rationale: Single source of truth for reading token files. The SDK owns file reading (read, trim, error messages), consumers just provide a path.
10. No
LINEAR_PROFILEenv varThe PR proposed
LINEAR_PROFILEenv var support. We're not doing this. Profile is not a secret — it doesn't need the env var treatment that tokens get (avoidingps auxleaks). The--profileflag is sufficient.Implementation Plan
Step 1: SDK changes (
crates/lineark-sdk/)src/auth.rs:token_from_file()signature totoken_from_file(path: &Path) -> Result<String, LinearError>auto_token()(it combined env + hardcoded path)token_file_path()helper (hardcoded~/.linear_api_token)token_from_env()unchangedtoken_from_file(&Path)signaturesrc/client.rs:Client::auto()Client::from_file()Client::from_token_file(path: &Path) -> Result<Self, LinearError>Client::from_token()andClient::from_env()unchangedsrc/lib.rs:src/helpers.rs:Client::auto()README.md:autorow, addfrom_token_filerowStep 2: CLI changes (
crates/lineark/)src/main.rs:--profileflag toClistruct withconflicts_with = "api_token"src/commands/usage.rs:~/.linear_api_token_*, striptest, display found profiles--profileREADME.md:Step 3: test-utils refactor (
crates/lineark-test-utils/)src/token.rs:lineark_sdk::auth::token_from_file(&path)test_token()andno_online_test_token()public API unchangedStep 4: Tests
SDK unit tests (
crates/lineark-sdk/src/auth.rs):token_from_filewith valid file (tempfile)token_from_filewith missing file (good error message with path)token_from_filewith empty filetoken_from_filetrims whitespacetoken_from_envunchanged testsCLI offline tests (
crates/lineark/tests/offline.rs):--profile workwith valid~/.linear_api_token_work(use tempdir + HOME override)--profile workwith missing file → error message includes profile name, available profiles, creation hint--profile+--api-token→ clap error--helpshows--profileflaglineark usageshows discovered profiles~/.linear_api_token$LINEAR_API_TOKENenv varStep 5: Documentation
lineark usageoutput/update-docsbefore mergingOut of Scope
LINEAR_PROFILEenv var — not needed,--profileflag sufficestomldependency — not neededVersion Impact
This is a breaking change to the SDK's public API (
Client::auto()andClient::from_file()removed). Bump to next major version.References