This document describes Granola CLI's security architecture, including credential handling, data storage, and security measures in place.
Granola CLI uses platform-native secure storage for credentials via the cross-keychain library:
- macOS: Keychain
- Windows: Credential Manager
- Linux: Secret Service
Storage Details:
- Service name:
com.granola.cli - Account:
credentials - Format: JSON containing
refreshToken,accessToken, andclientId
The CLI supports importing credentials from the Granola desktop application:
- Source:
~/Library/Application Support/Granola/supabase.json - Format: Supabase JSON format
- Process: Parses and validates required fields before storing in keychain
The CLI automatically handles token expiration:
Location: lib/auth.ts, lib/lock.ts, services/client.ts
How it works:
refreshAccessToken()calls WorkOS API to refresh expired tokenswithTokenRefresh()wrapper automatically retries operations on 401 errors- All API-calling service methods are wrapped with automatic token refresh
- New credentials are saved immediately after refresh
Token Rotation Flow:
- API call fails with 401 (Unauthorized)
withTokenRefreshcatches the error and callsrefreshAccessToken()- File-based lock is acquired to prevent race conditions (via
lib/lock.ts) - Credentials are re-read inside the lock (another process may have updated them)
- CLI sends refresh request to
https://api.workos.com/user_management/authenticate - WorkOS returns new
access_tokenand rotatedrefresh_token - New credentials are saved to keychain
- Lock is released
- Original operation is retried with fresh token
Important: WorkOS refresh tokens are single-use. Each refresh invalidates the previous token and issues a new one. The CLI uses file-based locking to prevent race conditions when multiple CLI processes attempt to refresh the token simultaneously.
Location: lib/lock.ts
When multiple CLI processes run concurrently (e.g., in scripts), they may both encounter expired tokens and attempt to refresh simultaneously. Without protection, this causes a race condition:
- Process A reads refresh token T1
- Process B reads refresh token T1
- Process A refreshes and invalidates T1, gets T2
- Process B tries to refresh with T1 (now invalid) and fails
The CLI prevents this using file-based locking:
- Lock file:
~/Library/Caches/granola/granola-token-refresh.lock(macOS) or system temp dir - Uses
O_CREAT | O_EXCLfor atomic lock acquisition - 30-second timeout waiting for lock
- Stale lock cleanup after 60 seconds
- Credentials are re-read inside the lock to use any updates from other processes
Non-sensitive configuration is stored using the conf library:
- Linux:
~/.config/granola/ - macOS:
~/Library/Application Support/granola/ - Format: Plaintext JSON (non-sensitive data only)
Location: lib/pager.ts
The CLI validates pager commands to prevent command injection:
- Whitelist of allowed pagers:
less,more,cat,head,tail,bat,most - Blocks commands containing shell metacharacters (
;,|,&,$,`, etc.) - Falls back to direct stdout output if pager command is invalid
const ALLOWED_PAGERS = ['less', 'more', 'cat', 'head', 'tail', 'bat', 'most'];
const SHELL_METACHARACTERS = /[;&|`$(){}[\]<>\\!#*?]/;
export function validatePagerCommand(cmd: string): boolean {
if (SHELL_METACHARACTERS.test(cmd)) return false;
const [binary] = cmd.split(' ');
const binaryName = binary.split('/').pop() || '';
return ALLOWED_PAGERS.includes(binaryName);
}Location: lib/config.ts, lib/alias.ts
Aliases are validated to prevent command injection:
parseAliasArguments()wrapper aroundshell-quotevalidateAliasCommand()rejects pipelines, redirects, or substitution syntax- Quoted arguments are supported, but only literal argv tokens reach Commander
export function parseAliasArguments(command: string): string[] {
const parsed = parseShellQuote(command);
if (parsed.length === 0) throw new Error('Alias cannot be empty');
if (parsed.some((token) => typeof token !== 'string')) {
throw new Error('Alias command contains unsupported shell syntax.');
}
return parsed as string[];
}Location: lib/auth.ts
Authentication errors are logged via the debug library:
- Developers can inspect credential-loading issues by enabling
granola:*namespaces - Use
DEBUGorGRANOLA_DEBUGenvironment variables - Maintains graceful degradation (returns null for non-blocking errors)
Example:
DEBUG=granola:lib:auth granola auth status| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Authentication required |
| 4 | Resource not found |
-
Never share your refresh token: Your refresh token provides long-term access to your account.
-
Logout on shared machines: Always run
granola auth logoutwhen finished using the CLI on shared or public computers. -
Be cautious with environment variables: Avoid setting
GRANOLA_PAGERto untrusted values or commands from unknown sources. -
Keep the CLI updated: Regularly update to the latest version to receive security patches and improvements.
-
Review stored credentials: Periodically verify that credentials are stored securely in your system's keychain.
If you discover a security vulnerability in Granola CLI, please report it responsibly:
- Do not open a public GitHub issue
- Contact the maintainers privately
- Provide detailed information about the vulnerability
- Allow reasonable time for a fix before public disclosure