Use GitHub App User Access Token with ghtkn to communicate with GitHub without storing secrets locally

Use GitHub App User Access Token with ghtkn to communicate with GitHub without storing secrets locally

Instead of PAT, use GitHub App's User Access Token to build a CLI tool called "ghtkn" that utilizes the GitHub API without storing secrets locally. The only thing you keep on hand is the Client ID. Short-lived tokens are managed by the OS secret manager and expire in 8 hours.
2026.06.24

This page has been translated by machine translation. View original

When calling the GitHub API from a client terminal, you want to communicate securely without storing secrets locally as a supply chain attack countermeasure, right?
The CLI tool that's perfect for this is ghtkn.

ghtkn has the following three features.

1. No tokens stored locally

ghtkn only saves the GitHub App's Client ID, which is not a secret, locally. No tokens or private keys are needed. There's no problem committing it to Git.

apps:
  - name: my-org/dev
    client_id: Ivxxxxxxxxxxxxxxxx

2. Short-lived tokens are managed by the OS secret manager

ghtkn issues short-lived tokens via GitHub App's Device Flow and stores them in the OS secret manager (macOS Keychain / Windows Credential Manager, etc.).
Moreover, tokens expire in 8 hours, significantly reducing the risk of leakage.

3. Authorization is the AND of GitHub App and user

The permissions of the User Access Token used by ghtkn are the AND of the user and the GitHub App. Even if the user has broad permissions, they can be restricted on the GitHub App side.
This mechanism is a specification of GitHub App's User Access Token, and the documentation explicitly states the following three points:

  • Can only access resources that the user can access
  • Can only access resources that the App has permissions for
  • Can only access resources of accounts where the App is installed

Differences between ghtkn and PAT

Comparing with PAT, which is commonly used for GitHub API operations, makes it easier to understand ghtkn's positioning.

Aspect classic PAT fine-grained PAT ghtkn (GitHub App User Access Token)
Secret information stored locally Token itself Token itself None (Client ID only)
Token issuance and renewal User manually issues/reissues User manually issues/reissues ghtkn automatically retrieves/refreshes
Storage location Up to the user Up to the user OS secret manager
Expiration Arbitrary (can be unlimited) Arbitrary Expires in 8 hours
Organization-side control Allow/block all only Approval-based, expiration management Managed by App permissions and installation
Permission scoping Depends on user permissions Specified in detail per token Restricted by AND of App × user

Let's try it

We'll walk through the process of using ghtkn from macOS to obtain a GitHub App User Access Token and use it with the gh CLI.

Environment

  • OS : macOS 26.4.1
  • ghtkn : 0.2.5
  • gh : 2.93.0

Preparing the GitHub App

First, create a GitHub App. Create a new one from the organization's Settings > Developer settings > GitHub Apps, and configure the following:

  • GitHub App name: A unique name
  • Expire user authorization tokens: Enable
  • Enable Device Flow: Enable
  • Webhook: Disable
  • Permissions: None (leave as default)
  • Repository access: None (leave as default)
  • Where can this GitHub App be installed?: Only on this account (= private)

github-app-detail

Installing ghtkn

$ brew install suzuki-shunsuke/ghtkn/ghtkn

$ ghtkn init

Write the GitHub App's Client ID in $HOME/.config/ghtkn/ghtkn.yaml.

apps:
  - name: my-org/dev
    client_id: Ivxxxxxxxxxxxxxxxx

From token acquisition to use with gh CLI

Let's obtain a token via ghtkn. The first time, a browser will open and the Device Flow authorization will run.

$ ghtkn get

The application uses the device flow to generate your GitHub User Access Token.
Copy your one-time code: XXX-XXXX
This code is valid until 2026-06-23T18:01:51+09:00
Press Enter to open https://github.com/login/device in your browser (it opens automatically after 10 seconds)...
Jun 23 17:47:02.918 INF opened the browser program=ghtkn version=0.2.5 app_name=my-org/dev url=https://github.com/login/device
ghu_xxx

Enter the one-time code from the browser.

github-app-device-otp

Let's integrate with gh.

$ ghtkn get
ghu_XXX
$ REPO=my-org/demo-ghtkn
$ env GH_TOKEN=$(ghtkn get) gh issue create -R "$REPO" --title "Hello, ghtkn" --body "This is created by ghtkn"

Creating issue in my-org/demo-ghtkn

GraphQL: Could not resolve to a Repository with the name 'my-org/demo-ghtkn'. (repository)

This is because we didn't set up permissions and repositories during the initial installation.

Let's set Issues Read and write as a permission, configure the target repository, and run it again.

$ env GH_TOKEN=$(ghtkn get) gh issue create -R "$REPO" --title "Hello, ghtkn" --body "This is created by ghtkn"

Creating issue in my-org/demo-ghtkn

https://github.com/my-org/demo-ghtkn/issues/4

This time it succeeded.

In this way, you can start with minimal permissions, and if something is insufficient, use errors like Could not resolve to a Repository as clues to add permissions (Issues: Read and write) and install for the target repository.

Wrapping gh for ghtkn

Writing env GH_TOKEN=$(ghtkn get) gh ... every time is tedious. If you prepare a wrapper script, just running the gh command directly will transparently use the ghtkn token.

#!/usr/bin/env bash
set -eu
if [ -z "${GH_TOKEN:-}" ] && [ -z "${GITHUB_TOKEN:-}" ]; then
  GH_TOKEN="$(ghtkn get)"
  export GH_TOKEN
fi
exec /opt/homebrew/bin/gh "$@"

Place this script in a directory on your PATH (e.g., ~/bin/gh) and grant it execute permissions (chmod +x ~/bin/gh).

With this mechanism, every time the gh command is called via the wrapper script, ghtkn retrieves the token from cache and injects it into GH_TOKEN.

It can also be used for calls from Claude Code.

Checking the stored token (macOS)

The token is stored in the login Keychain as a generic password, with the Client ID as the key. The contents and metadata can be checked with the security command.

$ security dump-keychain login.keychain-db | grep -iB2 -A15 ghtkn
class: "genp"
attributes:
    0x00000007 <blob>="github.com/suzuki-shunsuke/ghtkn"
    0x00000008 <blob>=<NULL>
    "acct"<blob>="Ivxxx"
    "cdat"<timedate>=0x32303236303631343035333032355A00  "20260614053025Z\000"
    ...

Device Activation support

When you introduce ghtkn, you will encounter a screen prompting Device Activation like the following.

This is because $ ghtkn auth is implicitly executed in the background, and re-running $ ghtkn auth will resolve it.

A specification change to explicitly execute this device flow is proposed in the following issue.

Stop Automatically Starting the Device Flow · Issue #474 · suzuki-shunsuke/ghtkn

In closing

When operating GitHub via API from a client environment, PAT comes to mind immediately, but by using GitHub App's User Access Token, you can achieve the same thing by only managing the non-secret Client ID locally. ghtkn, introduced this time, is a nice wrapper around this. In particular, it can cover many of the use cases of fine-grained PAT.

Please note that unlike classic PAT, where you can operate everything with a single token, you need to grant permissions per organization (resource owner), just as with fine-grained PAT.

Also, please note that since ghtkn authenticates users via Device Flow, it cannot be used for non-interactive purposes such as CI/CD.

Procedures for changing permissions and adding repositories in an Organization

For a GitHub App already installed in an Organization, it is not enough for the user to simply change permissions in the App management screen. New permissions have not yet been applied to already installed Organizations, and separate approval from the Organization side is required.

In other words, permission changes and repository additions are divided into two steps: "request from the user side" and "approval from the Organization administrator side."

User side: Request permission changes and repository additions

  1. Open the GitHub App management screen (Settings > Developer settings > GitHub Apps)
  2. Change the required permissions (e.g., Issues: Read and write) in Permissions & events and save
  3. Upon saving, a permission approval request is sent to the installed Organization

Organization administrator side: Approve permissions and add repositories

  1. Open the Organization's Settings
  2. Open Third-party access > GitHub Apps
  3. Open Configure for the target App where the permission approval request (Review request) is displayed
  4. Review the requested permissions (Issues: Read and write) and repositories, then Accept

References

Share this article