ghtkn

module
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2025 License: MIT

README

ghtkn

License | Install | Usage

Stop risking token leaks - Use secure, short-lived GitHub tokens for local development

⚠️ The Security Problem

Are you still using Personal Access Tokens (PATs) or GitHub CLI OAuth tokens stored on your local machine? These long-lived tokens pose significant security risks:

  • Indefinite or months-long validity - A leaked token remains dangerous for extended periods
  • Broad permissions - Often configured with wide access for convenience
  • Difficult to rotate - Manual management leads to tokens being used far longer than they should

✅ The ghtkn Solution

ghtkn generates 8-hour User Access Tokens from GitHub Apps using Device Flow - a fundamentally more secure approach:

  • Short-lived tokens - Only 8 hours validity minimizes damage from any potential leak
  • No secrets required - Only needs a Client ID (which isn't secret), no Private Keys or Client Secrets
  • User-attributed actions - Operations are performed as you, not as an app
  • Automatic token management - Integrates with OS keychains for secure storage and reuse

ghtkn allows you to manage multiple GitHub Apps through configuration files and securely store tokens using Windows Credential Manager, macOS Keychain, or GNOME Keyring.

[!NOTE] In this document, we call Windows Credential Manger, macOS KeyChain, and GNOME Keyring as secret manager.

🚀 Getting Started

[!WARNING] As a prerequisite, a secret manager is required. It will work without it, but in that case, you'll need to generate access tokens via device flow every time.

  1. Install ghtkn
  2. Create a GitHub App
  • Enable Device Flow
  • Disable Webhook
  • Homepage URL: https://github.com/suzuki-shunsuke/ghtkn (You can change this freely. If you share the GitHub App in your development team, it's good to prepare the document and set it to Homepage URL)
  • Only on this account
  • Permissions: Nothing
  • Repositories: Nothing

You don't need to create secrets such as Client Secrets and Private Keys.

  1. Create a configuration file by ghtkn init and modify it
ghtkn init
  • Windows: %APPDATA%\ghtkn\ghtkn.yaml
  • macOS, Linux: ${XDG_CONFIG_HOME:-${HOME}/.config}/ghtkn/ghtkn.yaml
persist: true
apps:
  - name: suzuki-shunsuke/none
    client_id: xxx # Mandatory. GitHub App Client ID

[!NOTE]
The GitHub App Client ID is not a secret, so there's generally no problem writing it in plain text in local configuration files.

  1. Run ghtkn get and create a user access token
ghtkn get

https://github.com/login/device will open in your browser, so enter the code displayed in the terminal and approve it. Then a user access token starting with ghu_ is outputted. You can close the opened tab.

With Device Flow, access tokens cannot be generated in non-interactive environments like CI. ghtkn is primarily intended for local development.

If you run the same command immediately, it will now run without the authorization flow because ghtkn stores access tokens into the secret manager and reuse them.

ghtkn get
  1. Run gh issue create using the access token
REPO=suzuki-shunsuke/ghtkn # Please change this to your public repository
env GH_TOKEN=$(ghtkn get) gh issue create -R "$REPO" --title "Hello, ghtkn" --body "This is created by ghtkn"

Then it fails due to the permission error even if you have the permission.

GraphQL: Resource not accessible by integration (createIssue)

Please grant the permission issues:write to the GitHub App and run again, then it still fails. Please install the app to the repository and run again, then it succeeds. At this time, the issue creator will be you, not the App.

The permissions (Permissions and Repositories) of a user access token are held by both the authorized user (i.e. you) and the GitHub App. Therefore, as shown above, the GitHub App cannot perform operations that it is not permitted to perform, and conversely, the user cannot perform operations that they are not authorized to perform.

Wrapping commands

It's bothersome to run env GH_TOKEN=$(ghtkn get) gh ... everytime you run gh command. And it's difficult to fix gh commands in scripts. So it's useful if an access token can be passed to commands without env GH_TOKEN=$(ghtkn get).

There are two techniques:

  1. Wrap commands by shell functions
  2. Wrap commands by shell scripts
⚠ Caution of wrapping commands

Wrapping commands is useful, but also has some drawbacks. Always passing access tokens can be the security risk.

⚠ Caution of aqua users

If you use aqua and wrap aqua, an access token is always passed to any commands executed via aqua. When you run a command managed by aqua, the command is executed via aqua exec command. So if you wrap aqua and pass an access token to aqua, the access token is also passed commands managed by aqua. If any commands are abused, the access token can be leaked. So we think you shouldn't wrap aqua. There is no problem to pass an access token to commands such as aqua i, aqua up, and so on. But passing an access token to any commands is dangerous.

Wrapping arbitrary commands via shell functions

[!WARNING] Unfortunately, even if you define shell functions in files like .bashrc, they aren't available in scripts.

bash test.sh # In test.sh, functions defined in .bashrc aren't available.

To avoid this issue, please check shell scripts out.

You can write simple wrappers (shell functions) for arbitrary commands that require access tokens using ghtkn.

e.g.

gh() {
    env GH_TOKEN=$(ghtkn get) command gh "$@" # Be careful to use 'command' to avoid infinite loops
}

This way, when you run the gh command normally, the access token will be automatically passed to the gh command.

We can define multiple shell functions using for loop:

for name in gh aqua; do
  eval "
  ${name}() {
    local token=\"\$(ghtkn get)\"
    env GITHUB_TOKEN=\"\$token\" command ${name} \"\$@\"
  }
  "
done
$ which gh               
gh () {
        local token="$(ghtkn get)" 
        env GITHUB_TOKEN="$token" command gh "$@"
}

Wrapping arbitrary commands via shell scripts

For instance, if you want to wrap the command gh,

  1. Put the script gh into $PATH

To avoid the infinite loop, you need to specify the absolute path of gh in the script.

#!/usr/bin/env bash

set -eu

GH_TOKEN="$(ghtkn get)" 
export GH_TOKEN
exec /opt/homebrew/bin/gh "$@"
  1. Make the script executable
chmod +x ~/bin/gh

You can use helper scripts.

We don't think these helper scripts are ideal solutions. But they are useful to some extent.

  1. Copy helper scripts in $PATH
cp helpers/* ~/bin
  1. Create wrappers using helpers
ghtkn-gen-wrap "$(command -v gh)" # Wrap the command gh

Git Credential Helper

ghtkn >= v0.1.2

You can use ghtkn as a Git Credential Helper:

[credential]
	helper = !ghtkn git-credential

Use multiple apps

You can configure multiple GitHub Apps in the apps section of the configuration file and create and use different Apps for each Organization or User. By default, the one with default: true is used. If there's no default: true, the first App in apps is used.

You can specify the App by command line argument:

ghtkn get suzuki-shunsuke/write

The value is the app name defined in the configuration file. Alternatively, you can specify it with the environment variable GHTKN_APP. For example, it might be convenient to switch GHTKN_APP for each directory using a tool like direnv.

I check out my repositories from https://github.com/suzuki-shunsuke into the ~/repos/src/github.com/suzuki-shunsuke directory. I then place a .envrc file in that directory with the following content:

source_up

export GHTKN_APP=suzuki-shunsuke/write

Similarly, I place a .envrc file in ~/repos/src/github.com/aquaproj as well:

source_up

export GHTKN_APP=aquaproj/write

I've also set up a default App that has no permissions. While some might think an access token with no permissions is useless, it can still be used to read public repositories and helps you avoid hitting API rate limits compared to not using an access token at all. So, it's quite useful.

apps:
  - name: suzuki-shunsuke/none
    client_id: xxx
    default: true

With this setup, the access token is transparently switched depending on the working directory. What's written in the .envrc is the GHTKN_APP, not the access token itself, which is safe because it's not a secret.

Access Token Regeneration

ghtkn stores generated access tokens and their expiration dates in the secret manager. ghtkn get retrieves these, and if the expiration has passed, regenerates the access token through Device Flow. The access token validity period is 8 hours.

By default, if the access token hasn't expired, it returns it, but this may result in a short-lived access token being returned. By specifying -min-expiration (-m) <minimum required validity period. Not a datetime but remaining time>, the access token will be regenerated if its validity period is shorter than the specified duration.

ghtkn get -m 1h

2h, 30m, 30s etc. are also valid. Units are required.

You can also set this using an environment variable.

export GHTKN_MIN_EXPIRATION=10m

If you're only using the GitHub CLI to call an API, it usually finishes in an instant, so you probably won't need to set this. However, if you're passing the access token to a script that takes, say, 30 minutes to run, setting it to something like 50m will prevent the token from expiring in the middle of the script.

By the way, if you set the value to 8 hours or more, you can replicate how ghtkn regenerates the access token. This could be useful if you want to test how ghtkn behaves.

Using ghtkn in Enterprise Organizations

When using ghtkn in a company's GitHub Organization, it may not be practical for each developer to create their own GitHub App in organizations with a certain scale. In such cases, you can create a shared GitHub App and share the Client ID within the company.

User Access Tokens cannot generate tokens with permissions beyond what the user has, and users cannot impersonate others. API rate limits are also per-user.

Therefore, the risk of sharing within a limited internal space is considered to be low.

From a company's perspective, this can prevent the leakage of developers' PATs or GitHub CLI OAuth App access tokens that have access to the company's Organization. Even if a Client ID is leaked outside the company, it doesn't provide direct access to the company's Organization, and even if an access token is leaked, the risk can be minimized due to its short validity period (8 hours).

Environment Variables

All environment variables are optional.

  • GHTKN_LOG_LEVEL: Log level. One of debug, info (default), warn, error.
  • GHTKN_OUTPUT_FORMAT: The output format of ghtkn get command
    • json: JSON Format
  • GHTKN_APP: The app identifier to get an access token
  • GHTKN_MIN_EXPIRATION: The minimum expiration duration of access token. If ghtkn get gets the access token from keying but the expiration duration is shorter than the minimum expiratino duration, ghtkn get creates a new access token via Device Flow
  • GHTKN_CONFIG: The configuration file path
  • XDG_CONFIG_HOME

Comparison between GitHub App User Access Token and other access tokens

GitHub CLI OAuth App access token

https://cli.github.com/manual/gh_auth_token

This can be easily generated with gh auth login, gh auth token in GitHub CLI. You don't need to generate Personal Access Tokens, and it's convenient. Also, when scopes across Users or Organizations are needed, it's difficult with non-Public GitHub Apps, but installing GitHub CLI OAuth App across multiple Users or Organizations solves such problems.

However, this access token is not very good from a security perspective. While you can restrict the scope (permission) and target Organizations, these tend to be quite broad for convenience. Also, it's basically indefinite. Therefore, the risk when this token is leaked is very high.

So, a more secure mechanism is needed.

fine-grained Personal Access Token

https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens

We'll ignore Legacy PAT as it's almost the same as OAuth App tokens.

Fine-grained access tokens have the following disadvantages compared to User Access Tokens:

  • Regular rotation is cumbersome
  • Management is cumbersome
  • High risk when leaked
    • While the validity period is not indefinite, it tends to be quite long
      • Since short periods make rotation cumbersome, it tends to be 1 year or 6 months
      • Not on the order of a few hours
GitHub App installation access token

https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation

  • Pros
    • Can change permissions, repositories, and validity period when generating tokens
  • Cons
    • Cannot operate as a User
      • e.g., PR creator becomes the App
    • Private Key management is cumbersome
    • High risk when Private Key is leaked

📝 Note

API rate limit

https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#primary-rate-limit-for-github-app-installations

Primary rate limits for GitHub App user access tokens (as opposed to installation access tokens) are dictated by the primary rate limits for the authenticated user. This rate limit is combined with any requests that another GitHub App or OAuth app makes on that user's behalf and any requests that the user makes with a personal access token. For more information, see Rate limits for the REST API.

The rate limit for authenticated users is 5,000 per hour, so it should be fine for normal use.

All of these requests count towards your personal rate limit of 5,000 requests per hour.

Limitation

ghtkn doesn't support some operations that require Client Secrets as the risk of Client Secret leakage is high:

Instead of refreshing a token, ghtkn regenerates the token through Device Flow. While you can't revoke a token directly with ghtkn, if you absolutely need to, you can either go to the GitHub App settings page and select "Revoke all user tokens" or temporarily generate a client secret and use the API to revoke the token.

LICENSE

MIT

Directories

Path Synopsis
cmd
gen-jsonschema command
ghtkn command
pkg
apptoken
Package apptoken handles GitHub App access token generation using OAuth device flow.
Package apptoken handles GitHub App access token generation using OAuth device flow.
cli
Package cli provides the command-line interface layer for ghtkn.
Package cli provides the command-line interface layer for ghtkn.
cli/flag
Package flag provides common command-line flags for ghtkn CLI.
Package flag provides common command-line flags for ghtkn CLI.
cli/get
Package get implements both the 'ghtkn get' command and 'ghtkn git-credential' command.
Package get implements both the 'ghtkn get' command and 'ghtkn git-credential' command.
cli/initcmd
Package initcmd implements the 'ghtkn init' command.
Package initcmd implements the 'ghtkn init' command.
config
Package config provides configuration management for ghtkn.
Package config provides configuration management for ghtkn.
controller/get
Package get provides functionality to retrieve GitHub App access tokens.
Package get provides functionality to retrieve GitHub App access tokens.
controller/initcmd
Package initcmd implements the business logic for the 'ghtkn init' command.
Package initcmd implements the business logic for the 'ghtkn init' command.
github
Package github provides a client for interacting with the GitHub API.
Package github provides a client for interacting with the GitHub API.
keyring
Package keyring provides secure storage for GitHub access tokens.
Package keyring provides secure storage for GitHub access tokens.
log
Package log provides structured logging functionality for ghtkn.
Package log provides structured logging functionality for ghtkn.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL