Working Directory (--cwd)#

Overview#

The --cwd (short: -C) global flag changes the logical working directory for all path resolution without mutating the process CWD. It behaves similarly to git -C:

scafctl --cwd /path/to/project run solution -f solution.yaml
scafctl -C /path/to/project run resolver -f demo.yaml -o json

When --cwd is set, all relative paths—including -f, --output-dir, snapshot paths, and auto-discovery—resolve against the specified directory instead of the process CWD.


Architecture#

Context-Based Resolution#

The --cwd flag is injected into the Go context.Context via provider.WithWorkingDirectory(), avoiding global state mutation. This design:

  • Is safe for concurrent use (MCP server handles multiple requests)
  • Does not affect other goroutines or the process environment
  • Composes cleanly with --output-dir (which operates on a separate context key)

Resolution Flow#

CLI:  --cwd flag → root PersistentPreRunE → provider.WithWorkingDirectory(ctx)
MCP:  cwd param  → s.contextWithCwd()     → provider.WithWorkingDirectory(ctx)
                                          provider.AbsFromContext(ctx, path)
                                          filepath.Join(cwd, relativePath)

Key Functions#

FunctionPackagePurpose
WithWorkingDirectorypkg/providerStores the working directory in context
WorkingDirectoryFromContextpkg/providerRetrieves the working directory from context
AbsFromContextpkg/providerResolves a relative path against the context working directory
GetWorkingDirectorypkg/providerReturns the effective working directory (context → os.Getwd() fallback)
ResolvePathpkg/providerFull path resolution including output-dir and cwd support

Interaction with --output-dir#

The --cwd and --output-dir flags serve different purposes and compose together:

FlagScopeAffects
--cwdAll phases (resolvers + actions)Where relative paths are resolved from
--output-dirAction phase onlyWhere action outputs are written to

When both are set:

  • Resolvers resolve paths against --cwd
  • Actions resolve output paths against --output-dir
  • A relative --output-dir itself is resolved against --cwd
# Resolvers read from /project, actions write to /project/out
scafctl --cwd /project run solution -f sol.yaml --output-dir out

MCP Server#

All MCP tools that accept file paths support an optional cwd parameter. When provided, it behaves identically to the CLI --cwd flag:

{
  "tool": "render_solution",
  "arguments": {
    "path": "solution.yaml",
    "cwd": "/path/to/project"
  }
}

Tools with cwd Support#

ToolFile
inspect_solutiontools_solution.go
lint_solutiontools_solution.go
render_solutiontools_solution.go
preview_resolverstools_solution.go
run_solution_teststools_solution.go
get_run_commandtools_solution.go
preview_actiontools_action.go
dryrun_solutiontools_dryrun.go
show_snapshottools_snapshot.go
diff_snapshotstools_snapshot.go
generate_test_scaffoldtools_testing.go
list_teststools_testing.go
diff_solutiontools_diff.go
build_plugintools_catalog_multiplatform.go
extract_resolver_refstools_refs.go

Tools without cwd (not applicable)#

ToolReason
list_examples / get_examplePaths reference embedded resources, not the filesystem
catalog_list / catalog_list_platformsCatalog references, not filesystem paths
list_providers / get_provider_schemaIn-memory registry lookups
list_cel_functions / evaluate_celNo filesystem paths
auth_statusNo filesystem paths

Fallback Behavior#

When --cwd is not set:

  1. WorkingDirectoryFromContext returns ("", false)
  2. AbsFromContext falls through to filepath.Abs() which uses os.Getwd()
  3. Behavior is identical to running without --cwd—fully backward compatible

Validation#

The --cwd value is validated at entry point (CLI root or MCP contextWithCwd):

  1. Resolved to absolute path via filepath.Abs
  2. Checked for existence via os.Stat
  3. Verified to be a directory (not a file)

Invalid values produce a clear error before any command logic executes.


Catalog Solution CWD#

When running a catalog solution (bare name), the bundle is extracted to a temporary directory and the process CWD is changed there so resolvers can read bundled files.

However, file-writing actions need to resolve relative paths against the caller’s original working directory — not the temporary bundle directory. The CLI injects the caller’s CWD into the action execution context via provider.WithWorkingDirectory(actionCtx, originalCwd). This ensures AbsFromContext resolves relative action paths against the caller’s CWD, matching the behaviour of local -f file runs.

Catalog run:  scafctl run solution my-app

1. User is in /projects/my-app (caller CWD)
2. Bundle extracted to /tmp/scafctl-bundle-xyz/
3. os.Chdir(/tmp/scafctl-bundle-xyz/)   ← resolvers read bundled files here
4. originalCwd captured as /projects/my-app
5. actionCtx = WithWorkingDirectory(ctx, originalCwd)
6. Actions resolve "./output" → /projects/my-app/output  ← correct

This aligns with how tools like npm init, cargo init, and cookiecutter work — they scaffold into the directory you run them from, regardless of where the template source lives.

When --output-dir is specified, it takes precedence over the context CWD for action path resolution (unchanged behaviour).