mcpbeat Sign in

Oauth Callback Not Through Nav Agent Skill

Deliver a returning auth callback's token straight to session state and let the login screen close itself by observing the stored session — routing the token through navigation pushes a second login screen and the post-login close peels the wrong one. Use when a browser-based login succeeds but the user is left staring at the login screen.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
100
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill oauth-callback-not-through-nav

The instruction itself

3 sections, as written by the author

The callback arrives at the app, not at a screen

A login that hands off to the system browser leaves the app running. The screen the user started

from is *still on the navigation stack* — nothing popped it, because nothing navigated away. When

the browser redirects back, the token arrives at the process, through whatever link mechanism

the platform uses (desktop-deep-link-plumbing covers getting it that far).

The tempting next move is to route it to the screen that needs it, by navigating to that screen with

the token as an argument. That pushes a second copy on top of the first. Login then succeeds,

the screen runs its "close myself" step, and that pops only the copy — landing the user on an

identical login screen with no error and no explanation. It reads as "logged in, but stuck on the

login page", and it gets investigated in the browser, in the redirect, in the service's dashboard —

anywhere but in the back stack, because nothing failed.

Two rules, and they only work together:

// adapted — names generalized: the link handler, at the app's entry point
if (uri.scheme == CALLBACK_SCHEME && uri.host == CALLBACK_HOST) {
    val token = uri.getQueryParameter("token")
    clearPendingLink()
    // Deliberately no navigation: the login screen is almost certainly already open — the browser
    // was opened from it — and navigating would stack a second copy on top of it.
    token?.let { sharedState.completeLogin(it) }
}
// adapted — names generalized: the login screen
// Closes on the *stored* session rather than on this screen's own state, because the redirect
// never reaches this screen.
LaunchedEffect(loggedIn) { if (loggedIn) navController.navigateUp() }

loggedIn is derived from the persisted session record, not from a result the screen was handed.

The screen is a passive observer of a fact stored elsewhere, which is what makes it correct no

matter who completed the login.

Traps

The screen may not exist by the time the token comes back. The user left for a browser; on a

memory-constrained platform the process can be killed and restarted behind them, and the callback

then arrives at a cold app with no login screen anywhere. Anything that assumes "the screen that

started this is still composed" — a callback lambda, a screen-scoped state holder, a suspending

function awaiting a result — is broken for that path. Session state must live above the screen and

survive the trip.

Screens that embed a web view never hit this, which is why it looks like a one-off. An in-app

web view keeps the whole flow inside the process, so the screen genuinely does receive the result

and the naive wiring works. The bug only appears on flows that hand off to the *system* browser —

the normal shape on a desktop host, where a real embedded browser is often unavailable. A codebase

with several logins will have the embedded-web-view ones working and the browser-handoff one not,

and the difference is not the service.

Only one of the two request-token flows ends in a redirect. Services that mint a request token

usually offer both: either you send the user to the authorization page with *no* token and the

service mints one and redirects to your registered callback, or you fetch the token yourself first

and open the authorization page with it already attached. The second tells the service that your app

already holds the token — so it renders a "you can return to the application now" page and **never

calls your callback**. Choosing it produces a flow that looks entirely healthy up to the last step

and then silently never completes, which is indistinguishable from a broken scheme registration.

Pick the redirect flow whenever you have a registered callback.

Do not let a link normalizer rewrite the callback. Apps that canonicalize incoming links into

one internal form will happily rewrite the callback too, and the token reader — which matches on the

original scheme and host — silently stops matching. Exclude the callback shape from the rewrite, and

match it *before* the general link handling, not after.

Clear the pending link before completing. Whatever cached the incoming URL for delivery will

replay it on the next composition or the next restore if it is not cleared, re-submitting an

already-spent token and producing a spurious failure toast over a successful login. Clear first,

complete second.

Assume the token is single-use and short-lived. It buys exactly one session; the second

exchange fails. That makes every accidental duplicate delivery — a replayed cache, a second

LaunchedEffect pass, a re-created screen — visible as a failure *after* a success, which is a

confusing pair of toasts. One delivery point, one exchange.

Give the user a way in when no handler exists. On a host with no scheme handler at all the

browser simply lands on the callback URL and shows it in the address bar. A field on the login

screen that accepts that pasted URL and pulls the token out of it reuses the parser you already have

and turns a dead end into a workaround — route it into the *same* completion function, never a

second one.

Verifying it

Find the callback entry point and confirm nothing between it and the session store navigates:

grep -rn "getQueryParameter\|\.scheme ==\|\.host ==" --include="*.kt" . | grep -v "/build/"
grep -rn "navigate(" --include="*.kt" . | grep -v "/build/"

Then confirm the login screen closes on stored state rather than on a handed-in result:

grep -rn -A3 "LaunchedEffect(loggedIn)\|LaunchedEffect(isLoggedIn)" --include="*.kt" . | grep -v "/build/"

Behaviourally, run the login three ways, because they are three code paths: complete it with the app

still in the foreground; complete it after backgrounding the app long enough for the process to be

killed; and complete it by pasting the callback URL by hand. All three must end on the screen the

user was on *before* the login screen. If any of them lands on a login screen, inspect the back

stack — the failure is a duplicate entry, not the login.

How to use it

Copy the folder

Take maxrave-dev/oauth-callback-not-through-nav from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.