If you discover a security vulnerability in localmost, please report it through GitHub Security Advisories.
Please do not open public issues for security vulnerabilities.
When reporting, please include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Any suggested fixes (optional)
- Acknowledgment: Within 1 week of report
- Initial assessment: Within 2 weeks of report
- Fix timeline: Depends on severity; critical issues prioritized
We follow coordinated disclosure. If you report a vulnerability, we ask that you give us 90 days to address it before public disclosure.
Only the latest release receives security updates. Users should always run the latest version.
- The localmost application (Electron app, main/renderer processes)
- Credential storage and handling
- Sandbox and network isolation mechanisms
- IPC between processes
- Authentication flows
- GitHub Actions Runner binary itself - Report vulnerabilities in the runner binary to GitHub. However, vulnerabilities in localmost's sandboxing or network isolation of the runner are in scope.
- Workflow code - Security of workflows you write is your responsibility
- Third-party dependencies - Report upstream, but please let us know so we can update
Security fixes are communicated through:
- GitHub Security Advisories
- Release notes on GitHub Releases
This section describes the security design of localmost.
localmost is an Electron desktop application that manages GitHub Actions self-hosted runners. It handles sensitive credentials and executes external binaries, requiring careful security considerations.
- Filesystem writes: Workflows cannot write to files outside the runner directory and temp paths
- Home directory access: Workflows cannot access
~/.ssh,~/.aws,~/.config, or other sensitive dotfiles - Network exfiltration: Workflows can only connect to allowlisted hosts (GitHub, npm, PyPI, etc.)
- Credential exposure: OAuth tokens are encrypted at rest using macOS Keychain
- GitHub's infrastructure: OAuth, API responses, and runner binary distribution are trusted. If GitHub is compromised, localmost provides no additional protection.
- Malware on your machine: If your system is already compromised, localmost cannot protect you.
- A compromised GitHub account: If an attacker has access to your GitHub account, they can modify workflows that run on your runner.
- Allowlisted hosts: Data can be exfiltrated to any host on the network allowlist (GitHub, npm, etc.).
- OAuth Device Flow: Uses GitHub's Device Flow for user authentication, appropriate for desktop applications that cannot securely store client secrets
- Token Management: Access tokens and refresh tokens are obtained via the GitHub App OAuth flow
- Token Refresh: Expired tokens are automatically refreshed using refresh tokens
- Required Permissions:
Administration: Read & Write- Register and remove self-hosted runners on repositoriesActions: Read & Write- Check workflow status and cancel running jobsMetadata: Read- Access basic repository information (required by GitHub for all apps)Self-hosted runners: Read & Write(org-level) - Register runners at the organization level
- Location: Configuration stored in
~/.localmost/config.yaml - Encryption: Sensitive tokens (access token, refresh token) are encrypted using Electron's
safeStorageAPI- Uses macOS Keychain for secure storage
- Encryption key is managed by the operating system and tied to the user account
- Encrypted values are stored with an
encrypted:prefix followed by base64-encoded ciphertext
- Fail-secure: Plaintext credentials are rejected; users must re-authenticate if OS encryption is unavailable
- Non-sensitive data: Settings like theme, runner count, and repository URLs remain in plaintext for easy user editing
- Access Control: The
~/.localmostdirectory and all contents are user-only (700 for directories, 600 for files). The app setsumask(077)at startup to ensure no group or world access.
This app uses encryption solely for secure credential storage via OS-provided APIs:
| Platform | Encryption Provider | Implementation |
|---|---|---|
| macOS | Apple Keychain Services | Via Electron safeStorage |
No custom cryptographic algorithms are implemented. The app delegates all encryption to macOS Keychain APIs.
This usage qualifies for:
- ECCN 5D992: Mass-market encryption exemption
- EAR Note 4: Exemption for authentication and access control
- Apple App Store: No additional export compliance documentation required (uses Apple-provided encryption only)
The application implements Electron security best practices:
- Context Isolation: Enabled (
contextIsolation: true) - renderer cannot access Node.js - Node Integration: Disabled (
nodeIntegration: false) - renderer runs in browser sandbox - Preload Scripts: Uses
contextBridge.exposeInMainWorld()for safe IPC - Electron Fuses: Security fuses configured:
RunAsNode: false - prevents using Electron as Node.jsEnableCookieEncryption: trueEnableNodeOptionsEnvironmentVariable: falseEnableNodeCliInspectArguments: falseEnableEmbeddedAsarIntegrityValidation: trueOnlyLoadAppFromAsar: true
- ASAR Packaging: Application code is packaged in ASAR archive
- Single Instance Lock: Prevents multiple instances from running simultaneously
- External Link Handling: External URLs open in system browser, not Electron
The application enforces a strict CSP header for the renderer:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data: https://avatars.githubusercontent.com;
connect-src 'self';
font-src 'self';
frame-src 'none';
object-src 'none'
Key security features:
- No
unsafe-inline: All styles are in external CSS files; dynamic styling uses CSS classes and data attributes - No
unsafe-eval: No use ofeval(),new Function(), or similar dynamic code execution - Restricted sources: Only same-origin resources allowed;
img-srcincludesavatars.githubusercontent.comfor displaying user profile images in the UI - No WebSocket directives: The app uses Electron IPC for all process communication
- Frame/Object blocking: Prevents embedding of iframes and plugins
- Source: Downloads official GitHub Actions runner from
github.com/actions/runnerreleases - Integrity Verification: Downloads are verified using SHA256 checksums from GitHub's release API
- Checksum is fetched from GitHub's official release notes
- Downloaded tarball hash is computed and compared before extraction
- Download is rejected if checksums don't match, preventing corrupted or tampered binaries
- Note: The runner binaries use adhoc code signatures (no verified identity), so we don't verify signatures—the checksum provides equivalent integrity assurance
- This verification model trusts GitHub's infrastructure, which localmost already relies on for OAuth and API access
- Execution: Runner binary is spawned as a child process with controlled environment
- Process Management: Child processes are managed via Node.js ChildProcess handles
- Processes are spawned with
detached: falseso they terminate when parent exits - Stop/cleanup uses direct process handles stored in the instances Map
- Stale process cleanup on startup uses path-specific matching (
~/.localmost/runner.*Runner.Listener) to avoid affecting unrelated processes
- Processes are spawned with
- Directory Isolation: Each runner instance has its own working directory
localmost adds isolation layers that the stock GitHub Actions Runner lacks:
| Resource | Access Level |
|---|---|
| File system (write) | Runner working directory and temp dirs only |
| File system (read) | Essential system paths (/usr/bin, /System/Library, Xcode) |
| Network | Allowlisted hosts only (GitHub, npm, PyPI, etc.) via HTTP proxy |
| Home directory | Denied — no access to ~/.ssh, ~/.aws, etc. |
| Other applications | Denied — no access to /Applications (except Xcode) |
| Resource | Access Level |
|---|---|
| Environment variables | All variables in runner process |
| Process spawning | Can spawn any executable in allowed paths |
| Mach/IPC | System frameworks require this |
The sandbox is not VM-level isolation. It primarily restricts filesystem writes:
- Network: Proxied through an allowlist, but the allowlist is broad (GitHub, npm, PyPI, Docker Hub, etc.). A malicious workflow could exfiltrate data to any allowlisted host.
- Process spawning: Allowed for any executable in permitted paths. CI runners genuinely require this capability.
- Mach/IPC: Allowed because system frameworks require it. This is a fundamental macOS constraint.
- Read access: Broader than write access—runners can read from
/usr/bin,/System/Library, Xcode, etc.
The sandbox reduces attack surface but does not provide full containment. For untrusted code, don't use a self-hosted runner.
| Repository Type | Risk Level | Recommendation |
|---|---|---|
| Private repos you control | Low | Safe—you're running your own code |
| Private repos with external contributors | Medium | Review PRs carefully before running CI |
| Public repos | High | Not recommended—any PR can run arbitrary code |
| Forks | High | Forked repo workflows can be modified maliciously |
| Feature | GitHub-Hosted | localmost |
|---|---|---|
| Fresh environment | New VM each job | Sandbox rebuilt fresh each start |
| Filesystem isolation | VM boundary | sandbox-exec restricts writes |
| Network isolation | VM boundary | Proxy allowlist |
| Credential isolation | No access to host | Home directory denied |
The sandbox is rebuilt fresh on each runner start and confines all writes to the runner directory and temp paths. Workflows cannot modify files elsewhere on your system or exfiltrate data to non-allowlisted hosts.
localmost includes a user filter that restricts which GitHub users' jobs are accepted:
| Mode | Description |
|---|---|
| Everyone | Accept jobs triggered by any user (default) |
| Just me | Only accept jobs triggered by the authenticated user |
| Allowlist | Only accept jobs from specific GitHub usernames |
When a job is triggered by a user not matching the filter, localmost automatically cancels the workflow run.
- Only use for private repositories you control
- Review all workflow changes before they run
- Disable "Run workflows from fork pull requests" in repo settings
- Use the user filter to restrict which users' jobs run locally
For more information on self-hosted runner security, see:
- GitHub: Security hardening for GitHub Actions
- Praetorian: Self-Hosted GitHub Runners Are Backdoors
- Synacktiv: GitHub Actions exploitation
The runner availability check uses a GitHub Actions variable instead of requiring API tokens in workflows:
- Repository/Org Variable: localmost updates a
LOCALMOST_HEARTBEATvariable with the current timestamp - Minimal Data: Contains only an ISO 8601 timestamp - no secrets or sensitive information
- No Workflow Tokens: CI workflows read the variable directly without any authentication
- Staleness Detection: Heartbeat older than 90 seconds indicates runner is offline
- Update Frequency: Heartbeat is updated every 60 seconds while runners are active
- Automatic Setup: Variable is created/updated automatically when the runner starts
This approach simplifies workflows by:
- Allowing workflows to check runner availability without needing API tokens
- Using the same permissions already required for runner registration
- All IPC communication uses named channels defined in
shared/types.ts - Renderer can only invoke explicitly exposed methods via the preload script
- No direct access to Node.js APIs from renderer process
Log messages are sanitized before being written to disk or displayed:
- GitHub tokens (
ghp_*,gho_*, etc.) are redacted - JWT tokens are redacted
- GitHub registration tokens are redacted
- Encrypted values and bearer tokens are redacted
- Sanitization applies to both the log file and renderer display
Code signing is required for distribution to prevent tampering warnings and establish trust.
Certificates needed:
- Apple Developer Program membership ($99/year)
- "Developer ID Application" certificate for distribution outside App Store
- "Developer ID Installer" certificate if distributing PKG installers
Entitlements: See entitlements.plist for App Sandbox and Hardened Runtime configuration.
Forge config for signing and notarization:
packagerConfig: {
osxSign: {
identity: process.env.APPLE_IDENTITY,
hardenedRuntime: true,
entitlements: './entitlements.plist',
'entitlements-inherit': './entitlements.plist',
'gatekeeper-assess': false,
strictVerify: true,
},
osxNotarize: {
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
teamId: process.env.APPLE_TEAM_ID,
},
},CI environment variables:
APPLE_IDENTITY: Certificate name (e.g., "Developer ID Application: Your Name (TEAM_ID)")APPLE_ID: Apple ID email for notarizationAPPLE_APP_SPECIFIC_PASSWORD: App-specific password (not your Apple ID password)APPLE_TEAM_ID: 10-character Team ID from Apple Developer account
Notarization is required for macOS 10.15+ to avoid Gatekeeper warnings. Apple scans the signed app for malware before issuing a notarization ticket.
The app is code-signed and notarized by Apple. To verify:
codesign -dv --verbose=2 /Applications/localmost.appLook for:
Authority=Developer ID Application: Bright Fulton (8D3BFBJK55)TeamIdentifier=8D3BFBJK55
Runner binaries can be independently verified against GitHub's published checksums:
- Find the expected checksum at https://github.com/actions/runner/releases
- Compute the checksum of your downloaded runner:
shasum -a 256 ~/.localmost/runner/arc/v*/actions-runner-*.tar.gz
- Compare the hashes
Note: localmost performs this verification automatically during download.