claude-aws-mfa 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -22,6 +22,16 @@ Releases are automated via CI. To create a new release:
22
22
  Stable versions (e.g. `v1.2.0`) publish to npm `@latest`. Pre-release versions
23
23
  (e.g. `v1.2.0-rc.1`) publish to npm `@next` and create a GitHub pre-release.
24
24
 
25
+ ## [v1.3.0] - 2026-10-14
26
+
27
+ ### Added
28
+ - Configurable STS session name in the credentials dialog (defaults to `claude-aws-mfa`).
29
+
30
+ ## [v1.2.2] - 2026-10-09
31
+
32
+ ### Changed
33
+ - Changed GitHub username
34
+
25
35
  ## [v1.2.1] - 2026-08-10
26
36
 
27
37
  ### Fixed
@@ -32,7 +42,7 @@ Stable versions (e.g. `v1.2.0`) publish to npm `@latest`. Pre-release versions
32
42
  ## [v1.2.0] - 2026-08-10
33
43
 
34
44
  ### Added
35
- - **Claude Code profile awareness**: `--setup` now reads and writes `$CLAUDE_CONFIG_DIR/settings.json` rather than always `~/.claude/settings.json`, so a tool like [claude-as](https://github.com/p120ph37/claude-as) can give one project a Bedrock profile while others stay on OAuth. The saved config and lock file are scoped to the profile as well (`~/.config/claude-aws-mfa<encoded-config-dir>.json`), since separate profiles generally mean separate AWS accounts and a shared cached session would otherwise hand a profile credentials for the wrong account. The default `~/.claude` is unsuffixed, so existing installs keep their current config file with no migration. Since the lock is per-profile, two profiles can prompt at the same time, so the credential, error, and setup dialogs all name their profile in the window title and in a banner under the heading. The default config dir is named too — to a multi-profile user an unlabelled dialog reads equally as "this is the default profile" and "this build does not label profiles".
45
+ - **Claude Code profile awareness**: `--setup` now reads and writes `$CLAUDE_CONFIG_DIR/settings.json` rather than always `~/.claude/settings.json`, so a tool like [claude-as](https://github.com/aaronmeriwether/claude-as) can give one project a Bedrock profile while others stay on OAuth. The saved config and lock file are scoped to the profile as well (`~/.config/claude-aws-mfa<encoded-config-dir>.json`), since separate profiles generally mean separate AWS accounts and a shared cached session would otherwise hand a profile credentials for the wrong account. The default `~/.claude` is unsuffixed, so existing installs keep their current config file with no migration. Since the lock is per-profile, two profiles can prompt at the same time, so the credential, error, and setup dialogs all name their profile in the window title and in a banner under the heading. The default config dir is named too — to a multi-profile user an unlabelled dialog reads equally as "this is the default profile" and "this build does not label profiles".
36
46
  - **`--no-ui` mode**: skip the GUI entirely and drive the tool from the command line — useful for scripting and for letting an LLM operate the tool during setup/debugging. Prints credentials JSON to stdout on success, keeping stdout cleanly parsable; on failure, prints the underlying error (or names the specific missing config field(s) when the config is incomplete) to stderr. Never imports the GUI toolkit, so it also works in headless environments.
37
47
  - **Error dialog**: authentication failures (STS or MFA command) now show a native dialog box instead of writing to stderr, since Claude Code doesn't surface a failing command's stderr to the user. Suppressed in `--no-ui` mode, where the error is printed to stderr instead.
38
48
 
package/README.md CHANGED
@@ -47,7 +47,7 @@ Add this (plus any addtitional config you prefer) to your Claude Code settings (
47
47
  ## How it works
48
48
 
49
49
  1. On first run, seeds the dialog with credentials from your existing AWS config (`~/.aws/credentials`, environment variables, SSO, etc.)
50
- 2. Shows a native GUI dialog with fields for region, access key, secret key, MFA ARN, role ARN, and session duration
50
+ 2. Shows a native GUI dialog with fields for region, access key, secret key, MFA ARN, role ARN, session duration, and STS session name (defaults to `claude-aws-mfa`)
51
51
  3. Collects the MFA token — either type a 6-digit code directly, or switch to **MFA Command** mode and provide a shell command that outputs the code (e.g. `op item get --otp …` for 1Password CLI)
52
52
  4. Calls `sts:AssumeRole` with the MFA token
53
53
  5. If the requested session duration is rejected, automatically retries with shorter durations (12h → 6h → 2h → 1h)
@@ -58,7 +58,7 @@ On subsequent runs, all fields are pre-populated from the saved config — just
58
58
 
59
59
  ### Claude Code profiles
60
60
 
61
- Claude Code reads its settings from `$CLAUDE_CONFIG_DIR` when that is set, which tools such as [claude-as](https://github.com/p120ph37/claude-as) use to give each project its own credentials. This tool follows suit:
61
+ Claude Code reads its settings from `$CLAUDE_CONFIG_DIR` when that is set, which tools such as [claude-as](https://github.com/aaronmeriwether/claude-as) use to give each project its own credentials. This tool follows suit:
62
62
 
63
63
  - `--setup` reads and writes `$CLAUDE_CONFIG_DIR/settings.json`, so a profile can be Bedrock-only while others stay on OAuth.
64
64
  - The config file is scoped per profile — `~/.config/claude-aws-mfa<profile>.json`, where `<profile>` is the config dir path encoded the way Claude encodes project dirs (`/` and `.` both become `-`). Different profiles usually mean different AWS accounts, and a shared cached session would hand a profile credentials for the wrong one.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "claude-aws-mfa",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
- "repository": "https://github.com/p120ph37/claude-aws-mfa",
5
+ "repository": "https://github.com/aaronmeriwether/claude-aws-mfa",
6
6
  "bin": {
7
7
  "claude-aws-mfa": "./src/index.ts"
8
8
  },
package/src/config.ts CHANGED
@@ -28,6 +28,7 @@ export interface Config {
28
28
  mfaArn: string;
29
29
  roleArn: string;
30
30
  duration: number;
31
+ sessionName?: string;
31
32
  mfaMode?: "code" | "command";
32
33
  mfaCommand?: string;
33
34
  cacheSession?: boolean;
@@ -1,8 +1,8 @@
1
- import { STANDARD_DURATIONS } from "./sts";
1
+ import { DEFAULT_SESSION_NAME, STANDARD_DURATIONS } from "./sts";
2
2
  import { AUTOSIZE_CSS, AUTOSIZE_SCRIPT } from "./autosize-html";
3
3
  import { DEFAULT_PROFILE, type ActiveProfile } from "./config";
4
4
 
5
- export const FIELDS = ["region", "accessKeyId", "secretAccessKey", "mfaArn", "roleArn", "duration", "mfaMode", "mfaCode", "mfaCommand"] as const;
5
+ export const FIELDS = ["region", "accessKeyId", "secretAccessKey", "mfaArn", "roleArn", "duration", "sessionName", "mfaMode", "mfaCode", "mfaCommand"] as const;
6
6
 
7
7
  export const FIELD_PATTERNS: Record<string, string> = {
8
8
  region: "^[a-z]{2}(-[a-z]+-\\d+)$",
@@ -11,6 +11,8 @@ export const FIELD_PATTERNS: Record<string, string> = {
11
11
  mfaArn: "^arn:aws:iam::\\d{12}:mfa/.+$",
12
12
  roleArn: "^arn:aws:iam::\\d{12}:role/.+$",
13
13
  duration: "^[1-9]\\d*$",
14
+ // AWS RoleSessionName constraint.
15
+ sessionName: "^[\\w+=,.@-]{2,64}$",
14
16
  mfaCode: "^\\d{6}$",
15
17
  mfaCommand: "^.+$",
16
18
  };
@@ -90,6 +92,7 @@ ${PROFILE_STYLE}
90
92
  <div class="field"><label>MFA Device ARN</label><input id="mfaArn" placeholder="arn:aws:iam::123456789012:mfa/user"></div>
91
93
  <div class="field"><label>Role ARN</label><input id="roleArn" placeholder="arn:aws:iam::123456789012:role/MyRole"></div>
92
94
  <div class="field"><label>Session Duration (seconds)</label><input id="duration" placeholder="${STANDARD_DURATIONS[0]}"></div>
95
+ <div class="field"><label>Session Name</label><input id="sessionName" placeholder="${DEFAULT_SESSION_NAME}"></div>
93
96
  <hr>
94
97
  <div class="radio-field">
95
98
  <input type="radio" name="mfaMode" id="mfaModeCode" value="code" checked>
package/src/dialog.ts CHANGED
@@ -3,6 +3,7 @@ import { lib } from "webview-bun/src/ffi";
3
3
  import { activeProfile, type Config } from "./config";
4
4
  import { buildHtml, buildErrorHtml } from "./dialog-html";
5
5
  import { START_HEIGHT, bindAutosize } from "./autosize";
6
+ import { DEFAULT_SESSION_NAME } from "./sts";
6
7
  import { version } from "../package.json";
7
8
 
8
9
  const DIALOG_WIDTH = 440;
@@ -24,6 +25,7 @@ export interface DialogResult {
24
25
  mfaArn: string;
25
26
  roleArn: string;
26
27
  duration: string;
28
+ sessionName: string;
27
29
  mfaMode: "code" | "command";
28
30
  mfaCode: string;
29
31
  mfaCommand: string;
@@ -81,6 +83,7 @@ export function showDialog(defaults: Partial<Config>): DialogResult | null {
81
83
  mfaArn: defaults.mfaArn ?? "",
82
84
  roleArn: defaults.roleArn ?? "",
83
85
  duration: defaults.duration != null ? String(defaults.duration) : "",
86
+ sessionName: defaults.sessionName ?? DEFAULT_SESSION_NAME,
84
87
  mfaCommand: defaults.mfaCommand ?? "",
85
88
  mfaMode: defaults.mfaMode ?? "code",
86
89
  }, activeProfile()));
package/src/index.ts CHANGED
@@ -92,6 +92,7 @@ async function tryAutoMfa(cfg: Partial<Config>): Promise<CachedSession | null> {
92
92
  roleArn: cfg.roleArn,
93
93
  mfaCode,
94
94
  duration: cfg.duration ?? STANDARD_DURATIONS[0],
95
+ sessionName: cfg.sessionName,
95
96
  });
96
97
 
97
98
  const session: CachedSession = {
@@ -108,6 +109,7 @@ async function tryAutoMfa(cfg: Partial<Config>): Promise<CachedSession | null> {
108
109
  mfaArn: cfg.mfaArn,
109
110
  roleArn: cfg.roleArn,
110
111
  duration,
112
+ sessionName: cfg.sessionName,
111
113
  mfaMode: cfg.mfaMode,
112
114
  mfaCommand: cfg.mfaCommand,
113
115
  cacheSession: cfg.cacheSession,
@@ -146,6 +148,7 @@ async function obtainViaDialog(cfg: Partial<Config>): Promise<void> {
146
148
  roleArn: result.roleArn,
147
149
  mfaCode,
148
150
  duration: parseInt(result.duration, 10) || STANDARD_DURATIONS[0],
151
+ sessionName: result.sessionName,
149
152
  });
150
153
 
151
154
  const session: CachedSession = {
@@ -162,6 +165,7 @@ async function obtainViaDialog(cfg: Partial<Config>): Promise<void> {
162
165
  mfaArn: result.mfaArn,
163
166
  roleArn: result.roleArn,
164
167
  duration,
168
+ sessionName: result.sessionName || undefined,
165
169
  mfaMode: result.mfaMode,
166
170
  mfaCommand: result.mfaCommand || undefined,
167
171
  cacheSession: defaults.cacheSession,
package/src/sts.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
2
2
 
3
+ export const DEFAULT_SESSION_NAME = "claude-aws-mfa";
4
+
3
5
  export const STANDARD_DURATIONS = [129600, 86400, 43200, 28800, 21600, 14400, 7200, 3600];
4
6
 
5
7
  export function isDurationError(err: unknown): boolean {
@@ -25,6 +27,7 @@ export async function assumeRoleWithMfa(params: {
25
27
  roleArn: string;
26
28
  mfaCode: string;
27
29
  duration: number;
30
+ sessionName?: string;
28
31
  }) {
29
32
  const client = new STSClient({
30
33
  region: params.region,
@@ -37,7 +40,7 @@ export async function assumeRoleWithMfa(params: {
37
40
  try {
38
41
  const { Credentials: c } = await client.send(new AssumeRoleCommand({
39
42
  RoleArn: params.roleArn,
40
- RoleSessionName: "claude-aws-mfa",
43
+ RoleSessionName: params.sessionName || DEFAULT_SESSION_NAME,
41
44
  SerialNumber: params.mfaArn,
42
45
  TokenCode: params.mfaCode,
43
46
  DurationSeconds: ladder[i],
@@ -103,7 +103,7 @@ describe("buildHtml", () => {
103
103
  test("FIELDS list matches expected form fields", () => {
104
104
  expect(FIELDS).toEqual([
105
105
  "region", "accessKeyId", "secretAccessKey",
106
- "mfaArn", "roleArn", "duration", "mfaMode", "mfaCode", "mfaCommand",
106
+ "mfaArn", "roleArn", "duration", "sessionName", "mfaMode", "mfaCode", "mfaCommand",
107
107
  ]);
108
108
  });
109
109
 
@@ -128,6 +128,7 @@ describe("buildHtml", () => {
128
128
  mfaArn: "arn:aws:iam::123456789012:mfa/user",
129
129
  roleArn: "arn:aws:iam::123456789012:role/TestRole",
130
130
  duration: "43200",
131
+ sessionName: "claude-aws-mfa",
131
132
  mfaCode: "123456",
132
133
  mfaCommand: "op item get --otp myitem",
133
134
  };
@@ -138,6 +139,7 @@ describe("buildHtml", () => {
138
139
  mfaArn: "not-an-arn",
139
140
  roleArn: "not-an-arn",
140
141
  duration: "abc",
142
+ sessionName: "has spaces",
141
143
  mfaCode: "12",
142
144
  mfaCommand: "",
143
145
  };
package/test/sts.test.ts CHANGED
@@ -117,6 +117,12 @@ describe("assumeRoleWithMfa", () => {
117
117
  });
118
118
  });
119
119
 
120
+ test("passes a custom session name to STS", async () => {
121
+ await assumeRoleWithMfa({ ...BASE_PARAMS, sessionName: "alice-laptop" });
122
+
123
+ expect(capturedCommands[0].RoleSessionName).toBe("alice-laptop");
124
+ });
125
+
120
126
  test("falls back to shorter duration on duration error", async () => {
121
127
  let callCount = 0;
122
128
  mockSend = mock(() => {