From 80a8fa32fbf2d60e22e8a4de0dc53a69e31bed09 Mon Sep 17 00:00:00 2001 From: Vijay Govindarajan Date: Sat, 28 Mar 2026 22:09:27 -0700 Subject: [PATCH 1/4] feat: support multiple --path flags in run command Allow specifying multiple --path flags to load and merge secrets from multiple folder paths when running a command. Later paths take precedence over earlier paths for duplicate secret keys. Example: infisical run --path=/global --path=/app-specific -- npm start This loads secrets from /global first, then /app-specific, with app-specific values overriding global ones for any conflicts. Fixes Infisical/infisical#900 Signed-off-by: Vijay Govindarajan --- packages/cmd/run.go | 83 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/packages/cmd/run.go b/packages/cmd/run.go index fecbccb1..8cb26e40 100644 --- a/packages/cmd/run.go +++ b/packages/cmd/run.go @@ -121,7 +121,7 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secretsPath, err := cmd.Flags().GetString("path") + secretsPaths, err := cmd.Flags().GetStringArray("path") if err != nil { util.HandleError(err, "Unable to parse flag") } @@ -136,25 +136,46 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - request := models.GetAllSecretsParameters{ - Environment: environmentName, - WorkspaceId: projectId, - TagSlugs: tagSlugs, - SecretsPath: secretsPath, - IncludeImport: includeImports, - Recursive: recursive, - ExpandSecretReferences: shouldExpandSecrets, - } + var injectableEnvironment models.InjectableEnvironmentResult + for i, secretsPath := range secretsPaths { + request := models.GetAllSecretsParameters{ + Environment: environmentName, + WorkspaceId: projectId, + TagSlugs: tagSlugs, + SecretsPath: secretsPath, + IncludeImport: includeImports, + Recursive: recursive, + ExpandSecretReferences: shouldExpandSecrets, + } - injectableEnvironment, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, token) - if err != nil { - util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") + pathEnvironment, fetchErr := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, token) + if fetchErr != nil { + util.HandleError(fetchErr, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") + } + + if i == 0 { + injectableEnvironment = pathEnvironment + } else { + // Merge: later paths override earlier paths for duplicate keys + injectableEnvironment.Variables = mergeEnvVars(injectableEnvironment.Variables, pathEnvironment.Variables) + injectableEnvironment.SecretsCount += pathEnvironment.SecretsCount + } } log.Debug().Msgf("injecting the following environment variables into shell: %v", injectableEnvironment.Variables) if watchMode { - executeCommandWithWatchMode(command, args, watchModeInterval, request, projectConfigDir, secretOverriding, token) + // Watch mode uses the first path for change detection + watchRequest := models.GetAllSecretsParameters{ + Environment: environmentName, + WorkspaceId: projectId, + TagSlugs: tagSlugs, + SecretsPath: secretsPaths[0], + IncludeImport: includeImports, + Recursive: recursive, + ExpandSecretReferences: shouldExpandSecrets, + } + executeCommandWithWatchMode(command, args, watchModeInterval, watchRequest, projectConfigDir, secretOverriding, token) } else { if cmd.Flags().Changed("command") { command := cmd.Flag("command").Value.String() @@ -220,10 +241,42 @@ func init() { runCmd.Flags().Int("watch-interval", 10, "interval in seconds to check for secret changes") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") - runCmd.Flags().String("path", "/", "get secrets within a folder path") + runCmd.Flags().StringArray("path", []string{"/"}, "get secrets within a folder path (can be specified multiple times to merge secrets from multiple paths)") runCmd.Flags().String("project-config-dir", "", "explicitly set the directory where the .infisical.json resides") } +// mergeEnvVars merges two slices of environment variables in KEY=VALUE format. +// Variables from the override slice take precedence over base for duplicate keys. +func mergeEnvVars(base, override []string) []string { + envMap := make(map[string]string) + var orderedKeys []string + + for _, entry := range base { + if parts := strings.SplitN(entry, "=", 2); len(parts) == 2 { + if _, exists := envMap[parts[0]]; !exists { + orderedKeys = append(orderedKeys, parts[0]) + } + envMap[parts[0]] = parts[1] + } + } + + for _, entry := range override { + if parts := strings.SplitN(entry, "=", 2); len(parts) == 2 { + if _, exists := envMap[parts[0]]; !exists { + orderedKeys = append(orderedKeys, parts[0]) + } + envMap[parts[0]] = parts[1] + } + } + + merged := make([]string, 0, len(orderedKeys)) + for _, key := range orderedKeys { + merged = append(merged, key+"="+envMap[key]) + } + + return merged +} + // Will execute a single command and pass in the given secrets into the process func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string) error { command := args[0] From c55b682ee7e3be3dcfdf9061884e9009ef08f661 Mon Sep 17 00:00:00 2001 From: Vijay Govindarajan Date: Sat, 28 Mar 2026 22:11:58 -0700 Subject: [PATCH 2/4] feat: expose INFISICAL_PROJECT_ID as env variable in run command Inject the resolved project ID as INFISICAL_PROJECT_ID into the subprocess environment, making it available alongside INFISICAL_TOKEN for CI/CD pipelines and automation scripts that need to reference the project without additional CLI flags. Fixes Infisical/infisical#2912 Signed-off-by: Vijay Govindarajan --- packages/cmd/run.go | 5 +++++ packages/util/constants.go | 1 + 2 files changed, 6 insertions(+) diff --git a/packages/cmd/run.go b/packages/cmd/run.go index 8cb26e40..1599fa48 100644 --- a/packages/cmd/run.go +++ b/packages/cmd/run.go @@ -526,6 +526,11 @@ func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, proje // check to see if there are any reserved key words in secrets to inject filterReservedEnvVars(secretsByKey) + // expose the project ID so downstream tools and CI/CD pipelines can reference it + if request.WorkspaceId != "" { + environmentVariables[util.INFISICAL_PROJECT_ID_NAME] = request.WorkspaceId + } + // now add infisical secrets for k, v := range secretsByKey { environmentVariables[k] = v.Value diff --git a/packages/util/constants.go b/packages/util/constants.go index 1ce6c850..4bf0c9e7 100644 --- a/packages/util/constants.go +++ b/packages/util/constants.go @@ -7,6 +7,7 @@ const ( INFISICAL_DEFAULT_EU_URL = "https://eu.infisical.com" INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" + INFISICAL_PROJECT_ID_NAME = "INFISICAL_PROJECT_ID" INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN" INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase. From 2ee1f1c2d7861fa2f16d5837acdb3a9f5c1a2523 Mon Sep 17 00:00:00 2001 From: Vijay Govindarajan Date: Sat, 28 Mar 2026 22:31:28 -0700 Subject: [PATCH 3/4] fix: resolve project ID from workspace config and prevent overwriting - Resolve project ID from .infisical.json when --projectId flag is not provided, so INFISICAL_PROJECT_ID is injected in all auth flows - Move project ID injection after the secrets loop so user secrets cannot overwrite it Signed-off-by: Vijay Govindarajan --- packages/cmd/run.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/cmd/run.go b/packages/cmd/run.go index 1599fa48..fd4f5468 100644 --- a/packages/cmd/run.go +++ b/packages/cmd/run.go @@ -82,6 +82,21 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } + // Resolve project ID from workspace config if not provided via flag + if projectId == "" { + if projectConfigDir != "" { + workspaceConfig, configErr := util.GetWorkSpaceFromFilePath(projectConfigDir) + if configErr == nil { + projectId = workspaceConfig.WorkspaceId + } + } else { + workspaceConfig, configErr := util.GetWorkSpaceFromFile() + if configErr == nil { + projectId = workspaceConfig.WorkspaceId + } + } + } + command, err := cmd.Flags().GetString("command") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -526,16 +541,16 @@ func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, proje // check to see if there are any reserved key words in secrets to inject filterReservedEnvVars(secretsByKey) - // expose the project ID so downstream tools and CI/CD pipelines can reference it - if request.WorkspaceId != "" { - environmentVariables[util.INFISICAL_PROJECT_ID_NAME] = request.WorkspaceId - } - // now add infisical secrets for k, v := range secretsByKey { environmentVariables[k] = v.Value } + // expose the project ID after secrets so it cannot be overwritten by user secrets + if request.WorkspaceId != "" { + environmentVariables[util.INFISICAL_PROJECT_ID_NAME] = request.WorkspaceId + } + env := make([]string, 0, len(environmentVariables)) for key, value := range environmentVariables { env = append(env, key+"="+value) From 667597c9d941dd50f33e2accb58f599275ba91ef Mon Sep 17 00:00:00 2001 From: Vijay Govindarajan Date: Sat, 28 Mar 2026 22:51:12 -0700 Subject: [PATCH 4/4] chore: retrigger CI Signed-off-by: Vijay Govindarajan