Skip to content

Allow configuring a custom API endpoint per host #13717

Open

Description

@dziemba

Describe the feature or problem you'd like to solve

gh lets you configure which GitHub host to use, but the API endpoint is always derived from that host — there's no way to route API traffic to a different endpoint.

Virtually every other tool in the GitHub ecosystem already treats the API endpoint as a separate configuration:

gh is the last commonly used tool in the ecosystem that doesn't let you configure where API requests go.

This matters in practice when an organization runs an API gateway or reverse proxy in front of GitHub — for access logging, rate-limit visibility, or request-level monitoring. Web UI and git clone/push traffic can stay on the canonical hostname (OAuth/SSO callbacks are host-bound, and git remotes work as-is), but API traffic is machine-to-machine and can be routed through a different endpoint. Every API client can be pointed at the gateway except gh.

It's especially relevant for GHEC data-residency tenants, where the *.ghe.com DNS zone is GitHub-managed and the organization has no control over the api.* hostname — routing API calls through a different endpoint is the only option, and gh doesn't support it.

Proposed solution

Add an optional API endpoint override to the per-host configuration in hosts.yml:

example.ghe.com:
    user: pete
    api_host: api-gateway.example.net

When set, gh sends REST and GraphQL requests to that host instead of the derived default. Authentication is unchanged. When not set, behavior is identical to today.

Additional context

Workarounds that people try today don't hold up:

  • git config url.*.insteadOf — only applies to git transport; gh's API HTTP client doesn't read it (see #12916)
  • Forking gh — works but means tracking every upstream release indefinitely across all platforms

The derivation logic lives in internal/ghinstance/host.go (RESTPrefix, GraphQLEndpoint) — the change seems contained. The api_host field above is one possible shape; we're open to whatever approach fits gh's config model best as long as there's a way to route API traffic to a different endpoint. We use gh at scale and are happy to contribute a PR if the maintainers are on board with the direction.

Activity

  1. babakks commented on Jun 24, 2026

    @babakks
    Member

    Thanks for your feedback, @dziemba! 🙏

    Providing a way for mapping hostnames has been under our radar somehow. Recently we opened #12928, to explore an env var based approach for granular token selection. However, what you're suggesting here kind of falls into the same concept. So, I encourage you to have a read at #12928 and share your thoughts with us.

    Note

    I know what you're suggesting here is more about a configuration option, but config vs env var is just a matter of exposure of a functionality. The best for gh is to offer them equally as each serve a different class of use cases (e.g. env vars tend to be more favourable in automated environments/infrastructure, while configuration is more appealing to human users).


    @BagToad and I had a quick chat and we're ideating about expanding the env vars in #12928 to allow overriding base API URLs as well:

    GH_TOKEN_example_ghe_com=<token>
    GH_API_URL_example_com=https://api.example.com
    GH_HOST_TYPE_example_come=ghe.com
    

    This is not exactly a mapping approach, mostly because gh doesn't really need to know the exact target hostname (i.e. example.ghe.com), but I believe this would serve your use case.

    For your scenario, I can imagine you'd just need to populate GH_API_URL_* and GH_HOST_TYPE_*, and the token selection would be left to the gh credential selection mechanism.

    Feel free to comment your thoughts, @BagToad and @dziemba.

  2. dziemba commented on Jun 25, 2026

    @dziemba
    Author

    Thanks for the quick reply. I'm glad you're interested in this idea!

    GH_TOKEN_example_ghe_com=<token>
    GH_API_URL_example_com=https://api.example.com
    GH_HOST_TYPE_example_come=ghe.com
    

    This looks great and combines this request nicely with #12928. I believe it would solve our use-case.

    Ideally we'd also have the hosts.yaml option in addition to the env overrides. In some environments, especially developer laptops, it would be easier to just set the overrides once in the file, instead of having to ensure that the environment variable overrides propagate everywhere (e.g. different shell setups, AI coding tools, IDEs etc.).

  3. added
    enhancementa request to improve CLI
    and removed on Jun 29, 2026
  4. github-actions commented on Jun 29, 2026

    @github-actions
    Contributor

    Thank you for your issue! We have categorized it as an enhancement (feature or improvement) request, and it has been added to our backlog. In doing so, we are not committing to implementing this feature at this time, but, we will consider it for future releases based on community feedback and our own product roadmap.

    Unless you see the help wanted Contributions welcome label, we are not currently looking for external contributions for this feature.

    If you come across this issue and would like to see it implemented, please add a thumbs up! This will help us prioritize the feature. Please only comment if you have additional information or viewpoints to contribute.

  5. added 6 commits that reference this issue on Jun 30, 2026
    0d96edd
    b330185
    b635ee1
    b3f21a8
    d7d4cc6
    788ed03
  6. williammartin commented on Jul 22, 2026

    @williammartin
    Member

    @dziemba one thing that's not clear to me in your setup (as per the PR you opened) is how you expect to handle URLs in http response headers or bodies. For example, given a GHEC deployment at company.ghe.com which you want to gateway at github.company.com, by default, URLs are going to point to company.ghe.com. A specific example would be pagination, which includes Link headers in the http response.

    It seems to me there are three ways this might be solved:

    • The gateway rewrites URLs in http content
    • GHEC is configured (not sure if this is possible) to serve as if it is on github.company.com
    • Every client has to rewrite URLs

    From your PR it seems like maybe the last point is what you're expecting but it seems like a lot of work to put on downstream consumers of your gateway. Maybe we're the only one? Am I understanding this correctly? Cheers.

  7. dziemba commented on Jul 23, 2026

    @dziemba
    Author

    Hey @williammartin
    AFAIK GHEC does not allow custom domains and has no way of rewriting URLs internally.

    We have solved this in our company by having the gateway rewrite URLs, both in Link headers and in the JSON responses. It was a bit tricky to get right but works perfectly fine now.

    So we would not expect the gh CLI to rewrite the URLs itself.

  8. davidlin-stripe commented on Jul 28, 2026

    @davidlin-stripe

    Hi!

    Thank you for proposing this, we've had similar issue before and the solution is largely identical to what we have here. Although, would it be worth consolidating this implementation to go-gh?

    In the case where user install custom extensions, for example gh-stack, we'll face this issue again, as it uses the HTTP client from the go-gh library, and just eye-balling from the implementation it seems to be farily close to the one hand-rolled in cli. But this implies a larger refactor on the cli, which might not be worth it.

  9. williammartin commented on Jul 28, 2026

    @williammartin
    Member

    Hey @davidlin-stripe, thanks for dropping by.

    Although, would it be worth consolidating this implementation to go-gh?

    Yep, 100%. That's one of my biggest concerns with the linked PR. I have this loose plan to consolidate API Client usage, which should then allow me to push API host mapping down into go-gh so that extensions can make use of it.

  10. williammartin commented on Aug 7, 2026

    @williammartin
    Member

    @dziemba @davidlin-stripe I am heading out on vacation for two weeks but I believe I have an artifact that y'all can play around with that is addressing most use cases. Specifically called out as not done yet are:

    • gh cs though it already supports GITHUB_API_URL
    • setting the value via gh set config or gh auth login --api-host
    • seeing the api host in gh auth status

    This Pull Request is really focused on migrating everything into a shared place that will respect this config. It's the big lift after the easy ones in:

    It absolutely requires more review and more poking at from me, but I really wanted to get something in your hands before I head out. I've been banging my head against it for a week now so you may find something sus in there, but I believe it's sufficient for you to get and play around with, to make sure it works as you expect, or to leave review comments on places that don't (after all, you have very bespoke setups).

    Quick demo

    Needs Go 1.26+ and make. This builds gh from the pull request and points it at your gateway using a throwaway config directory, so your real gh setup is untouched.

    #!/usr/bin/env bash
    set -uo pipefail
    
    HOST=example.ghe.com
    API_HOST=gateway.example.invalid
    TOKEN=ghp_thisisnotarealtoken
    
    git clone --depth 1 -b williammartin-api-host-commit-split \
        https://github.com/cli/cli gh-api-host-demo
    cd gh-api-host-demo
    make
    
    # A throwaway config dir, so none of this touches your real gh setup.
    export GH_CONFIG_DIR="$PWD/.ghconfig"
    mkdir -p "$GH_CONFIG_DIR"
    
    # version: "1" tells gh the config is already in its current format. Without it,
    # gh runs its multi-account migration over this hand-written hosts.yml on the
    # first command, finds no token where the old format kept one, and deletes the
    # host entry outright, api_host and all.
    printf 'version: "1"\n' > "$GH_CONFIG_DIR/config.yml"
    
    cat > "$GH_CONFIG_DIR/hosts.yml" <<EOF
    $HOST:
        api_host: $API_HOST
        user: tester
        users:
            tester:
                oauth_token: $TOKEN
    EOF
    
    # These would override the file above.
    unset GH_TOKEN GITHUB_TOKEN GH_HOST
    
    # Every request fails to connect, because gateway.example.invalid does not exist.
    # GH_DEBUG=api prints the request before it is sent, so we see where it was
    # addressed. Failure is expected, hence the `|| true`.
    show() { GH_DEBUG=api ./bin/gh "$@" 2>&1 | grep -E '^> (GET|POST|Host:)' | head -2 || true; }
    
    echo "== gh api, REST =="
    show api user --hostname "$HOST"
    echo "== gh api, GraphQL =="
    show api graphql --hostname "$HOST" -f query='query{viewer{login}}'
    echo "== gh run list, REST through the shared client =="
    show run list -R "$HOST/cli/cli"
    echo "== gh repo view, GraphQL through the shared client =="
    show repo view "$HOST/cli/cli"
    
    # The failure itself names the gateway too, which is the stronger proof: nothing
    # quietly fell back to api.example.ghe.com and succeeded behind our backs.
    echo "== the error names it too =="
    ./bin/gh repo view "$HOST/cli/cli" 2>&1 | grep -i 'error connecting' || true
    

    Should look like:

    ➜  cli-triaging ./api.sh
    Cloning into 'gh-api-host-demo'...
    remote: Enumerating objects: 1792, done.
    remote: Counting objects: 100% (1792/1792), done.
    remote: Compressing objects: 100% (1578/1578), done.
    remote: Total 1792 (delta 222), reused 949 (delta 135), pack-reused 0 (from 0)
    Receiving objects: 100% (1792/1792), 14.57 MiB | 6.34 MiB/s, done.
    Resolving deltas: 100% (222/222), done.
    GOOS= GOARCH= GOARM= GOFLAGS= CGO_ENABLED= go build -o script/build script/build.go
    go build -trimpath -ldflags "-X github.com/cli/cli/v2/internal/build.Date=2026-08-07 -X github.com/cli/cli/v2/internal/build.Version=9869df6 " -o bin/gh ./cmd/gh
    == gh api, REST ==
    > GET /user HTTP/1.1
    > Host: gateway.example.invalid
    == gh api, GraphQL ==
    > POST /graphql HTTP/1.1
    > Host: gateway.example.invalid
    == gh run list, REST through the shared client ==
    > GET /repos/cli/cli/actions/runs?exclude_pull_requests=true&per_page=20 HTTP/1.1
    > Host: gateway.example.invalid
    == gh repo view, GraphQL through the shared client ==
    > POST /graphql HTTP/1.1
    > Host: gateway.example.invalid
    == the error names it too ==
    error connecting to gateway.example.invalid
    

    Which demonstrates a few different paths that are correctly respecting api_host. Of course, it would be awesome if you could make the change in your own environments and see that it works correctly.

    Examples

    GHEC (ish)

    First, with no api_host key:

    ➜ cat ~/.config/gh/hosts.yml
    example.ghe.com:
        git_protocol: https
        users:
            williammartin:
        user: williammartin
    
    ➜ GH_DEBUG=api ./bin/gh api /user | cat
    * Request at 2026-08-07 21:22:35.563352 +0200 CEST m=+0.060591668
    * Request to https://api.example.ghe.com/user
    > GET /user HTTP/1.1
    > Host: api.example.ghe.com <-------- going to GHEC API
    

    Then with an api_host key:

    ➜  cat ~/.config/gh/hosts.yml
    example.ghe.com:
        git_protocol: https
        users:
            williammartin:
        user: williammartin
        api_host: api.github.com
    
    ➜ GH_DEBUG=api ./bin/gh api /user | cat
    * Request at 2026-08-07 21:25:22.663761 +0200 CEST m=+0.046389751
    * Request to https://api.github.com/user
    > GET /user HTTP/1.1
    > Host: api.github.com <---------- going to api_host
    

    GHES

    First, with no api_host key:

    ➜  cat ~/.config/gh/hosts.yml
    ghe.io:
        git_protocol: https
        users:
            williammartin:
        user: williammartin
    
    ➜ GH_DEBUG=api ./bin/gh api /user | cat
    * Request at 2026-08-07 21:35:28.430207 +0200 CEST m=+0.046331001
    * Request to https://ghe.io/api/v3/user
    > GET /api/v3/user HTTP/1.1
    > Host: ghe.io <-------- going to GHES API
    

    Then with an api_host key:

    ➜ cat ~/.config/gh/hosts.yml
    ghe.io:
        git_protocol: https
        users:
            williammartin:
        user: williammartin
        api_host: api-gateway.example.com
    
    ➜ GH_DEBUG=api ./bin/gh api /user | cat
    * Request at 2026-08-07 21:31:25.398173 +0200 CEST m=+0.043534460
    * Request to https://api-gateway.example.com/api/v3/user
    > GET /api/v3/user HTTP/1.1
    > Host: api-gateway.example.com <------ going to api_host
    

    Note that the api_host is a direct swap in the Host header, it's not trying to infer subdomain or path suffixes. I could imagine, particularly in the GHES case that it might be nice to drop the path.

    Cheers, and see you in a couple of weeks. Feel free to reach out to @babakks or @BagToad here in my absence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementa request to improve CLI

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions