plandex-ai/plandex · error
no access to plan
Error message
no access to plan
What it means
This 401 is returned by authorizePlan when ValidatePlanAccess succeeds but returns a nil plan, meaning the user is not a member of (or has no access to) the plan within their organization. Unlike 814, the lookup worked; the result was 'no access'. Plans are scoped by membership plus org, so cross-org or non-member access is rejected.
Source
Thrown at app/server/handlers/auth_helpers.go:682
}
return true
}
func authorizePlan(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {
log.Println("authorizing plan")
plan, err := db.ValidatePlanAccess(planId, auth.User.Id, auth.OrgId)
if err != nil {
log.Printf("error validating plan membership: %v\n", err)
http.Error(w, "error validating plan membership", http.StatusInternalServerError)
return nil
}
if plan == nil {
log.Println("user doesn't have access the plan")
http.Error(w, "no access to plan", http.StatusUnauthorized)
return nil
}
return plan
}
func authorizePlanUpdate(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {
plan := authorizePlan(w, planId, auth)
if plan == nil {
return nil
}
if plan.OwnerId != auth.User.Id && !auth.HasPermission(shared.PermissionUpdateAnyPlan) {
log.Println("User does not have permission to update plan")
http.Error(w, "User does not have permission to update plan", http.StatusForbidden)
return nil
}View on GitHub (pinned to e2d772072e)
Solutions
- Confirm the authenticated user has been added as a member of the plan
- Verify the planId belongs to the same organization as the auth token
- Have the plan owner add the user to the plan, or use an account with access
- Re-fetch the plan list for the current org and use a valid planId
Example fix
// before
await api.createBranch({ planId: urlPlanId });
// after
const plans = await api.listPlans();
if (!plans.some(p => p.id === urlPlanId)) {
throw new Error('You do not have access to this plan; ask the owner to add you as a member');
}
await api.createBranch({ planId: urlPlanId }); Defensive patterns
Strategy: validation
Validate before calling
async function ensurePlanAccess(planId) {
const plans = await api.listPlans();
if (!plans.some(p => p.id === planId)) {
throw new Error(`No access to plan ${planId}; ask the owner to add you as a member`);
}
} Type guard
function isNoPlanAccess(res) {
return res.status === 401;
} Try / catch
try {
return await api.getPlan(planId);
} catch (e) {
if (e.status === 401 && /no access to plan/.test(e.body)) {
notifyUser('You are not a member of this plan');
redirect('/plans');
return null;
}
throw e;
} Prevention
- Fetch plans scoped to the current org/session instead of reusing old IDs
- Invalidate cached plan IDs when membership is revoked or org switches
- Use invitation/share flows rather than sharing raw plan URLs
- Handle 401 from plan endpoints by re-listing accessible plans
When it happens
Trigger: Calling plan-scoped endpoints (update/delete/rename/archive via the authorizePlan* wrappers, ListBranchesHandler, CreateBranchHandler) with a planId the user was never added to, a plan in another org, or a plan removed while membership rows were deleted.
Common situations: Sharing a plan URL with a teammate who has no membership; switching orgs and reusing an old plan ID; a plan membership revoked but a stale client session still shows the plan; guessing plan IDs in API scripts.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- error validating project membership: %v
- error checking if project exists: %v
- error validating project
- project does not exist in org
- error validating plan membership
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/d0cd37a0e217a4f8.
Report an issue: GitHub.