Skip to content

fix: create default provider config on first run instead of fatal error (fixes #487) - #542

Merged
Mzack9999 merged 12 commits into
projectdiscovery:devfrom
XananasX7:fix/create-default-config-on-first-run
Jun 19, 2026
Merged

fix: create default provider config on first run instead of fatal error (fixes #487)#542
Mzack9999 merged 12 commits into
projectdiscovery:devfrom
XananasX7:fix/create-default-config-on-first-run

Conversation

@XananasX7

@XananasX7 XananasX7 commented Jun 15, 2026

Copy link
Copy Markdown

Summary

Fixes #487

Problem

On a fresh install (including Windows Server 2012 R2 which doesn't pre-create ~/.config/), running notify without a provider config produces:

[INF] Current notify version 1.0.7 (latest)
Using default provider config: C:\Users\Administrator\.config\notify\provider_config.yaml
[FTL] Could not create runner: file doesn't exist

The error is technically correct but unhelpful — nobody tells the user where to create the file or what it should contain.

Fix

Before calling SubstituteConfigFromEnvVars, check if the default config path exists. If not:

  1. Create the config directory (mkdir -p)
  2. Write a commented template config (matching the docs format) so the user has a starting point
  3. Print an [INF] message directing them to the file

The new first-run experience:

[INF] Current notify version 1.0.7 (latest)
Using default provider config: ~/.config/notify/provider-config.yaml
[INF] Created default provider config at ~/.config/notify/provider-config.yaml — please add your provider credentials.

Notes

  • The fix only triggers for the default config path (when -pc is not specified); explicit -pc paths still return an error if they don't exist
  • Write failure is logged as [WRN] and execution continues to give a clearer error downstream
  • No behaviour change when config already exists

Summary by CodeRabbit

  • New Features
    • Provider configuration is now auto-initialized on first run from a default template when no config file is present.
    • Parent directories are created automatically when needed.
    • Default templates and directories are written with secure permission settings; startup logs initialization outcome.
  • Bug Fixes
    • If a provider config path is explicitly set but missing, startup now fails fast with a clear “file not found” error.
    • Config files that are empty or comment-only no longer prevent startup; provider options remain unset.

iambouali and others added 8 commits January 7, 2025 22:25
…lack-username

Slack Provider: Fixes slack_username and adds slack_icon_emoji
…or (fixes projectdiscovery#487)

When notify is run for the first time without a provider config file
(the default is ~/.config/notify/provider-config.yaml), the runner
previously returned a fatal error:

  [FTL] Could not create runner: file doesn't exist

This is confusing because the user hasn't been told where to create
the file or what it should contain.

Fix: if the default config path does not exist, create the config
directory and write a commented template file.  A helpful INFO message
is printed pointing the user to the file they need to fill in.  If the
write fails (unlikely, but possible on read-only filesystems), a
warning is logged and execution continues so the error message is
clearer.

This also fixes the Windows Server 2012 R2 regression where the
.config/notify/ directory never existed and always caused a crash.
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 23183bdf-d88c-4789-9c16-f49602b75213

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

NewRunner in internal/runner/runner.go gains a first-run initialization block that detects whether the provider config path is default or user-specified. If the config file is missing and a path was explicitly provided, it returns an error; if using the default path, it creates the config directory (0700) and writes a default commented YAML template (0600), logging accordingly. YAML parsing is made tolerant of completely empty or comment-only config files by explicitly handling io.EOF. Three test cases validate config creation on first run, error handling for explicit missing paths, and successful parsing of comment-only files.

Changes

First-run provider config initialization

Layer / File(s) Summary
Path detection and config file initialization
internal/runner/runner.go, internal/runner/runner_test.go
NewRunner now distinguishes between default and user-specified config paths, checks whether the config file exists, and creates the parent directory (0700) and writes a default commented YAML template (0600) if missing, with corresponding log messages on success or failure. An explicit user-provided path that does not exist triggers an error instead of creation. Tests verify first-run creation with correct file permissions, error handling for explicit missing paths, and that the expected template is written.
Empty-config tolerant YAML parsing
internal/runner/runner.go, internal/runner/runner_test.go
YAML decoding is updated to explicitly handle io.EOF when the config is completely empty or contains only comments, treating this as valid and leaving providerOptions zero-valued, while still failing on actual parse errors. A test confirms that comment-only config files parse successfully without error.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐇 First-time user, take no fright,
No config file? The rabbit makes it right!
Default path gets a folder and a template so fine,
User path errors—that's the line!
Even empty comments now parse with grace,
A smoother start for every place! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: creating a default provider config on first run instead of fatal error, directly addressing issue #487.
Linked Issues check ✅ Passed The code changes fully implement the requirements: creating default config on first run with template, handling explicit missing paths with errors, and tolerating comment-only configs.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the provider config initialization logic and adding comprehensive tests; no unrelated modifications are present.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/runner/runner.go`:
- Around line 44-67: The code has two issues: (1) It does not distinguish
between default and user-specified config paths when deciding whether to create
a template. Before the os.Stat check on options.ProviderConfig, track a boolean
flag indicating whether the default path is being used (set this flag when
options.ProviderConfig was empty and defaulted), then only perform the template
creation logic when this flag is true, ensuring user-specified paths via the -pc
flag return an error if missing. (2) The default template contains only comments
and no actual provider configuration, causing the YAML parse to produce an empty
providerOptions struct that silently succeeds when passed to providers.New with
no error or indication to the user. Add validation after parsing the YAML to
reject empty provider configurations with a clear error message explaining that
the file was created as a template and the user must add provider credentials,
or alternatively update the template to include a valid minimal example (such as
an uncommented but placeholder provider entry) to guide users toward correct
configuration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b0eb5960-cdf8-4fae-bba8-e496467cd2bd

📥 Commits

Reviewing files that changed from the base of the PR and between 2d6ef20 and c3de69b.

📒 Files selected for processing (1)
  • internal/runner/runner.go

Comment thread internal/runner/runner.go Outdated
Comment on lines +44 to +67
// If the default config file does not exist yet, create the directory and
// write an empty/commented template so the user knows what to fill in.
// This avoids a fatal error on first run (e.g. fresh Windows install).
if _, statErr := os.Stat(options.ProviderConfig); os.IsNotExist(statErr) {
if mkdirErr := os.MkdirAll(filepath.Dir(options.ProviderConfig), 0700); mkdirErr != nil {
return nil, errors.Wrap(mkdirErr, "could not create provider config directory")
}
defaultConfig := `# notify provider configuration
# Fill in your provider details below. Full documentation:
# https://docs.projectdiscovery.io/tools/notify/provider-config
#
# Example:
# slack:
# - id: "my-slack"
# slack_webhook_url: "https://hooks.slack.com/services/..."
# slack_username: "notify"
# slack_format: "{{data}}"
`
if writeErr := os.WriteFile(options.ProviderConfig, []byte(defaultConfig), 0600); writeErr != nil {
gologger.Warning().Msgf("Could not create default provider config at %s: %s\n", options.ProviderConfig, writeErr)
} else {
gologger.Info().Msgf("Created default provider config at %s — please add your provider credentials.\n", options.ProviderConfig)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for the providers.New implementation to understand empty-options behavior
ast-grep --pattern 'func New($$$) $$$'

Repository: projectdiscovery/notify

Length of output: 12463


User-specified config paths incorrectly trigger template creation, violating PR objectives.

The PR objectives explicitly state: "The fix only applies when using the default config path (when -pc flag is not specified); explicitly specified config paths still return an error if missing." However, lines 44-67 do not distinguish between default and user-specified paths.

When a user runs notify -pc /custom/path.yaml and that file doesn't exist, this block will create the directory and write a template instead of returning an error. This directly contradicts the intended behavior. Lines 35-42 set options.ProviderConfig to the default only when empty, but once set (either by default or via -pc flag), line 47 treats both identically.

To fix, track whether the default path is being used before the existence check:

+	// Determine if we're using the default config path
+	defaultConfigPath := ""
+	if home, err := os.UserHomeDir(); err == nil {
+		defaultConfigPath = filepath.Join(home, types.DefaultProviderConfigLocation)
+	}
+	isDefaultPath := (options.ProviderConfig == defaultConfigPath)
+
 	// If the default config file does not exist yet, create the directory and
 	// write an empty/commented template so the user knows what to fill in.
 	// This avoids a fatal error on first run (e.g. fresh Windows install).
-	if _, statErr := os.Stat(options.ProviderConfig); os.IsNotExist(statErr) {
+	if isDefaultPath && _, statErr := os.Stat(options.ProviderConfig); os.IsNotExist(statErr) {

Empty provider config silently succeeds rather than failing with clear error.

The template at lines 51-61 contains only comments. After the file is created, line 74 parses this comment-only YAML, producing an empty providerOptions struct. When this empty struct is passed to providers.New at line 80, the function does not error—it silently returns a client with no providers configured. Users will then run notify with a freshly created config but receive no notification output and no error message, creating confusing UX where the tool appears to "work" but doesn't actually send anything.

Consider either (1) adding validation to reject empty provider configurations with a clear error message, or (2) including a valid minimal example in the template (e.g., an uncommented but placeholder provider entry) to guide users toward correct configuration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/runner/runner.go` around lines 44 - 67, The code has two issues: (1)
It does not distinguish between default and user-specified config paths when
deciding whether to create a template. Before the os.Stat check on
options.ProviderConfig, track a boolean flag indicating whether the default path
is being used (set this flag when options.ProviderConfig was empty and
defaulted), then only perform the template creation logic when this flag is
true, ensuring user-specified paths via the -pc flag return an error if missing.
(2) The default template contains only comments and no actual provider
configuration, causing the YAML parse to produce an empty providerOptions struct
that silently succeeds when passed to providers.New with no error or indication
to the user. Add validation after parsing the YAML to reject empty provider
configurations with a clear error message explaining that the file was created
as a template and the user must add provider credentials, or alternatively
update the template to include a valid minimal example (such as an uncommented
but placeholder provider entry) to guide users toward correct configuration.

…mments YAML

Per code review feedback:
- Track usingDefaultPath flag before applying the default; only create the
  template when the path was defaulted. User-specified paths that don't exist
  now return an explicit error instead of silently creating a template.
- Tolerate io.EOF from yaml.Decoder.Decode when the config file is empty or
  contains only comments (e.g. freshly-created default template), so first run
  no longer silently fails with a nil providerOptions.
@XananasX7

Copy link
Copy Markdown
Author

Applied the code review suggestions:

  1. Default vs user-specified path distinction — added usingDefaultPath flag. If the user explicitly passes -pc /some/path and it doesn't exist, we now return a clear error (provider config file not found: /some/path) instead of silently creating a template there.

  2. Empty/all-comments YAML — the freshly-created default template yields io.EOF from yaml.Decoder.Decode (no YAML nodes to decode). We now tolerate io.EOF and continue with a zero-valued providerOptions (no channels configured), consistent with the "first run — please fill in your credentials" experience.

@Mzack9999
Mzack9999 self-requested a review June 16, 2026 20:41
@Mzack9999
Mzack9999 changed the base branch from main to dev June 19, 2026 21:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/runner/runner_test.go`:
- Around line 39-43: The test currently only validates file permissions (0600)
for the config file but does not verify the parent directory permissions (0700)
which are created in runner.go. Within the same conditional block checking
runtime.GOOS != "windows", add an additional permission check for the parent
directory by obtaining its file info (similar to how you get info for the file)
and verifying that the directory mode permissions equal 0o700, logging an error
if they do not match.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a0e2061d-bf10-402a-93a8-1a7938f2346e

📥 Commits

Reviewing files that changed from the base of the PR and between 7238daa and b79507f.

📒 Files selected for processing (1)
  • internal/runner/runner_test.go

Comment on lines +39 to +43
if runtime.GOOS != "windows" {
if perm := info.Mode().Perm(); perm != 0o600 {
t.Errorf("config file perms = %o, want 0600", perm)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add directory permission check to validate 0700 on the parent directory.

The implementation creates the parent directory with 0700 permissions (as shown in context snippet runner.go:54), but the test only verifies file permissions. Directory permissions should also be validated since they're part of the security posture.

🔒 Proposed addition to verify directory permissions
 	if runtime.GOOS != "windows" {
 		if perm := info.Mode().Perm(); perm != 0o600 {
 			t.Errorf("config file perms = %o, want 0600", perm)
 		}
+		dirInfo, dirStatErr := os.Stat(filepath.Dir(options.ProviderConfig))
+		if dirStatErr != nil {
+			t.Fatalf("stat config directory: %v", dirStatErr)
+		}
+		if dirPerm := dirInfo.Mode().Perm(); dirPerm != 0o700 {
+			t.Errorf("config directory perms = %o, want 0700", dirPerm)
+		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if runtime.GOOS != "windows" {
if perm := info.Mode().Perm(); perm != 0o600 {
t.Errorf("config file perms = %o, want 0600", perm)
}
}
if runtime.GOOS != "windows" {
if perm := info.Mode().Perm(); perm != 0o600 {
t.Errorf("config file perms = %o, want 0600", perm)
}
dirInfo, dirStatErr := os.Stat(filepath.Dir(options.ProviderConfig))
if dirStatErr != nil {
t.Fatalf("stat config directory: %v", dirStatErr)
}
if dirPerm := dirInfo.Mode().Perm(); dirPerm != 0o700 {
t.Errorf("config directory perms = %o, want 0700", dirPerm)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/runner/runner_test.go` around lines 39 - 43, The test currently only
validates file permissions (0600) for the config file but does not verify the
parent directory permissions (0700) which are created in runner.go. Within the
same conditional block checking runtime.GOOS != "windows", add an additional
permission check for the parent directory by obtaining its file info (similar to
how you get info for the file) and verifying that the directory mode permissions
equal 0o700, logging an error if they do not match.

# Conflicts:
#	cmd/integration-test/integration.go
@Mzack9999
Mzack9999 merged commit ef5d328 into projectdiscovery:dev Jun 19, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Running notify error could not create runner in windows server r2 2012

5 participants