Automate Safe File Renaming with Rename Watch

Use Indexly rename-watch to monitor folders and rename incoming files safely with previews, filters, counters, quarantine, durable recovery, and audit logs.

Overview

indexly rename-watch turns a local folder into a rigorous, user-friendly rename pipeline. It can watch continuously or process one frozen batch, wait for incoming files to finish copying, select only the files you want, generate predictable names, prevent overwrites, retain monotonic counters, quarantine terminal failures, and recover safely after interruption.

Rename Watch is a standalone filesystem command. It does not index file contents, update the Indexly database, or change the behavior of rename-file or indexly watch. This separation makes it useful even when all you need is reliable local rename automation.

Goal Use this command Why
Rename a file or an existing folder on demand indexly rename-file Direct, interactive batch renaming with optional database sync and organizer handoff.
Rename new arrivals repeatedly from a JSON-defined pipeline indexly rename-watch Settling, retries, selection rules, counters, quarantine, recovery, and continuous modes.
Keep the full-text index updated when files change indexly watch Indexing workflow; it does not replace Rename Watch’s rename pipeline.

Quick start

These commands work in PowerShell, Command Prompt, Bash, and Zsh. Run them from the folder where you want to keep the configuration and its default inbox.

  1. Create a safe starter configuration and the inbox folder:

    indexly rename-watch --config "./rename-watch.json" --init
    
  2. Validate the configuration, its paths, runtime state, and lock availability:

    indexly rename-watch --config "./rename-watch.json" --check-config
    
  3. Add test documents to ./inbox, then preview one frozen batch:

    indexly rename-watch --config "./rename-watch.json" --once --dry-run
    
  4. Apply that one batch after reviewing the preview:

    indexly rename-watch --config "./rename-watch.json" --once
    
  5. Run continuously when the pipeline is ready for normal use:

    indexly rename-watch --config "./rename-watch.json"
    
  6. From another terminal, inspect its read-only durable status:

    indexly rename-watch --config "./rename-watch.json" --status
    
  7. For schema validation, portable paths, and safe migration, read Rename Watch Configuration.

  8. For unattended operation, continue with Operate Rename Watch as a Service.

Configuration example

--init never overwrites an existing configuration. It creates an inbox directory beside the JSON file and writes a standard document profile. The following expanded example also enables a terminal-failure quarantine:

{
  "version": 1,
  "service": {
    "shutdown_drain_timeout_seconds": 30,
    "health_interval_seconds": 5,
    "health_stale_after_seconds": 15
  },
  "jobs": [{
    "id": "inbox",
    "watch_path": "./inbox",
    "destination_subfolder": "processed",
    "pattern": "{date}-{title}-{counter}",
    "date_format": "%Y%m%d",
    "counter_format": "03d",
    "title_format": "standard",
    "mode": "hybrid",
    "scan_interval_seconds": 60,
    "settle_seconds": 3,
    "include": ["*.docx", "*.pdf", "*.txt", "*.md"],
    "exclude": ["Thumbs.db", "desktop.ini", ".DS_Store", ".thumbnails/"],
    "respect_indexlyignore": true,
    "recursive": false,
    "quarantine_subfolder": ".indexly-quarantine",
    "retry": {"max_attempts": 8, "initial_delay_seconds": 2, "max_delay_seconds": 60}
  }]
}

Relative paths are resolved from the configuration file, not from the shell’s current directory. A missing watch directory is created automatically, including missing parent directories. An existing non-directory or an inaccessible location is rejected. Each destination and quarantine must be a strict child of its watch folder. Every quarantine must also be disjoint from all configured destination and quarantine subtrees that it could overlap. Environment variables and leading ~ are expanded only for --config and watch_path; destination and quarantine subfolders remain literal relative children. See Rename Watch Configuration for the published JSON Schema and exact expansion contract.

Job settings

Setting Purpose Default when omitted
id Unique, case-sensitive job identifier used by operator commands and durable state. Required.
watch_path Folder from which eligible files are consumed. Relative paths start beside the JSON file. Required.
destination_subfolder Strict child of watch_path that receives completed files. Required.
pattern Output name built from {date}, {title}, {counter}, and {prefix}. {date}-{title}
date_format Format used by {date}. %Y%m%d
counter_format Python integer format such as 03d; must be empty when {counter} is absent. d with {counter}, otherwise empty.
title_format standard for lowercase kebab case or camel-case. standard
mode event, interval, or hybrid. hybrid
scan_interval_seconds Interval-mode rescan period in seconds. 60
settle_seconds Required unchanged period before a move is attempted. 3
include Root-relative POSIX globs that opt files in. All otherwise eligible files.
exclude Root-relative POSIX globs that opt files or subtrees out. No additional glob exclusions.
respect_indexlyignore Add rules from <watch_path>/.indexlyignore. false
recursive Select eligible files below the watch root. false
max_file_size_bytes Positive maximum size; a file exactly at the limit is accepted. No size limit.
quarantine_subfolder Optional protected child for files that reach a terminal failure. Disabled; failed files stay at source.
no_counter_collision_policy For patterns without {counter}: fail, quarantine, or leave-source. fail
retry.max_attempts Maximum bounded processing attempts. 8
retry.initial_delay_seconds First retry delay before exponential backoff. 2
retry.max_delay_seconds Maximum delay between retry attempts. 60

Service settings

The optional top-level service object applies to the continuous process, not to one individual job.

Setting Purpose Default when omitted
shutdown_drain_timeout_seconds Process-wide deadline for draining pending work after intake stops. 30
health_interval_seconds Interval between atomic live-status heartbeat updates. 5
health_stale_after_seconds Maximum healthy heartbeat age; must exceed the heartbeat interval. 15

All service values must be finite positive numbers. Existing version-1 configurations remain valid when the object is omitted. See service operation for the shutdown sequence and manager-timeout requirements.

Unknown keys and invalid combinations are rejected rather than ignored. Multiple jobs can share one configuration; when their watch roots overlap, Rename Watch validates each quarantine against every configured destination and quarantine before starting.

Choose a run mode

Mode How it finds files When to use it
hybrid Filesystem events plus periodic reconciliation scans. Recommended for normal operation; catches arrivals and files missed while copying or locked.
event Filesystem events only. Low-latency local workflows where the platform watcher is reliable and periodic rescans are not wanted.
interval Periodic reconciliation scans only. Network shares, mounted volumes, or environments where filesystem events are unavailable or unreliable.

The job’s mode is persistent. To override every configured job for one invocation without editing JSON, use one of these copy/paste-ready commands:

indexly rename-watch --config "./rename-watch.json" --mode event
indexly rename-watch --config "./rename-watch.json" --mode interval
indexly rename-watch --config "./rename-watch.json" --mode hybrid

The override controls file discovery during continuous operation. The parser also accepts it with --once, but a one-batch run always uses its frozen initial reconciliation set and starts neither observers nor periodic rescans, so changing the mode does not change that batch algorithm. --mode cannot be combined with --init, --check-config, --once --dry-run, or a state-operator action.

Command and flag guide

Flag or combination Use it when Example
--config PATH Select the required JSON configuration for every Rename Watch action. indexly rename-watch --config "./rename-watch.json" --status
--init Create a starter JSON file and adjacent inbox; refuses to overwrite. indexly rename-watch --config "./rename-watch.json" --init
--check-config Validate schema, paths, state access, recovery integrity, and lock availability without consuming files. indexly rename-watch --config "./rename-watch.json" --check-config
--once Process only the filesystem identities captured by the initial scan, then exit. indexly rename-watch --config "./rename-watch.json" --once
--once --dry-run Preview a frozen batch without moving files or consuming counters. indexly rename-watch --config "./rename-watch.json" --once --dry-run
--mode MODE Temporarily override all job modes for one invocation; meaningful for continuous discovery. indexly rename-watch --config "./rename-watch.json" --mode interval
--status Read configured jobs, recovery state, active failures, and retained history. indexly rename-watch --config "./rename-watch.json" --status
--health Check whether the live service heartbeat is present and fresh. indexly rename-watch --config "./rename-watch.json" --health --json
--readiness Check whether startup is complete and the service is accepting work. indexly rename-watch --config "./rename-watch.json" --readiness --json
--metrics Read current-process operational counters and gauges. indexly rename-watch --config "./rename-watch.json" --metrics --json
--export-service-template PLATFORM --output PATH Render a reviewed WinSW, systemd, launchd, or newsyslog starting point without installing it. indexly rename-watch --config "./rename-watch.json" --export-service-template systemd --output "./indexly-rename-watch.service" --service-user indexly --service-group indexly
--migrate-config --output PATH Validate version 1 and write a deterministic, non-overwriting canonical copy without expanding stored paths or injecting defaults. indexly rename-watch --config "./rename-watch.json" --migrate-config --output "./rename-watch.migrated.json"
--inspect-counters Read counter state for all jobs or one --job. indexly rename-watch --config "./rename-watch.json" --inspect-counters --job inbox
--reset-counters Reset one --date-key or --all-counters for one counter-enabled --job. indexly rename-watch --config "./rename-watch.json" --reset-counters --job inbox --date-key 20260716
--retry-failures Retry one durable --failure-id or a snapshot selected by --all-failures. indexly rename-watch --config "./rename-watch.json" --retry-failures --job inbox --failure-id 3f7bbf87-842b-4a68-a3a8-1450d36f47f5
--job ID Select an exact, case-sensitive job for counter or failure operations. indexly rename-watch --config "./rename-watch.json" --inspect-counters --job inbox
--yes Bypass a destructive operator confirmation; required for non-interactive and machine-readable reset/retry calls. indexly rename-watch --config "./rename-watch.json" --retry-failures --job inbox --all-failures --yes
--json Emit successful status, probe, metrics, migration, counter, reset, or retry output as one JSON document. indexly rename-watch --config "./rename-watch.json" --status --json
--json-errors Emit failures as one versioned JSON document on standard error. indexly rename-watch --config "./rename-watch.json" --status --json --json-errors

Action flags such as --status, --health, --readiness, --metrics, --export-service-template, --inspect-counters, --reset-counters, and --retry-failures, and --migrate-config are mutually exclusive. Run indexly rename-watch --help for the parser-level reference.

Continuous and one-batch behavior

Run continuously with indexly rename-watch --config "./rename-watch.json", or perform one reconciliation batch with --once. The destination is created only when a ready file is moved.

--once freezes the files found by its initial scan; files arriving later are left for the next invocation. It does not run periodic rescans, but it does wait for every initial file to settle and for the complete configured retry policy, including exponential backoff. It exits only after each initial file moved, disappeared externally, or produced one terminal failure record. A file that keeps changing beyond the bounded settle-and-retry window is left in place and logged with TimeoutError. Consequently, --once can run for the sum of all configured settle intervals and retry delays rather than returning after only one settle interval. The frozen snapshot uses filesystem identity, not only a pathname, so a later file appearing at the same path is not consumed. If the filesystem cannot provide a stable identity, --once leaves the file in place and applies the configured retry and terminal-failure policy.

Hybrid mode reacts to filesystem events and periodically scans for files missed while copied or locked. Rename Watch waits for a file to remain unchanged for the configured settling period, retries transient filesystem errors, and logs only completed moves or final failures under Indexly’s normal NDJSON log tree. For log storage, rotation, and analysis, see the Indexly Logging System.

File selection

New configurations created by --init use a standard document profile: *.docx, *.pdf, *.txt, and *.md. They also exclude common desktop metadata and thumbnail artifacts. These are explicit template values, not implicit defaults: an existing configuration that omits include and exclude continues to accept every otherwise eligible file.

include and exclude are root-relative POSIX glob lists. Patterns without a slash match a filename at any selected depth; matching is case-insensitive so the document profile also accepts names such as REPORT.PDF. Within a path, * stays in one segment and ** spans any number of directories. Matching normalizes composed and decomposed Unicode names. An exclude that matches a directory removes its subtree; a trailing slash makes that directory intent explicit. If a file matches both lists, exclude wins. Directories, symlinks or Windows reparse points, temporary files, and the destination subtree remain ineligible regardless of configured globs.

Set respect_indexlyignore to true to add rules from exactly <watch_path>/.indexlyignore to the job’s exclusions. Rename-watch does not search parent directories, recognize .indexignore, or substitute an ignore preset when the file is absent. The file uses Indexly’s existing ignore-rule semantics, including its platform case behavior and built-in Office lock-file rule. It is read once while the watch-root lock is held; restart rename-watch to apply later edits. An unsafe, unreadable, oversized, or non-UTF-8 ignore file fails closed. Rename-watch never creates or changes this file.

Create and inspect a standard .indexlyignore before starting Rename Watch:

indexly ignore init "./inbox"
indexly ignore show "./inbox" --source --effective
indexly rename-watch --config "./rename-watch.json" --check-config

Then keep respect_indexlyignore set to true. The explicit include and exclude lists still apply; .indexlyignore adds exclusions and never opts a file back in. See Ignore Rules & Index Hygiene for rule syntax, presets, inspection, and upgrade commands. Rename Watch uses the root file’s rule semantics, but its root-only loading and fail-closed safety checks are specific to this command.

Set recursive to true to select files below the watch root. The default is false. Recursive scans and filesystem events use the same policy and never follow linked directories. Add max_file_size_bytes as a positive integer to reject files larger than that many bytes; a file exactly at the limit remains eligible. Omitting it leaves file size unrestricted. Selection and size are checked again immediately before a move.

Validate and preview

Validate a configuration before starting the service:

indexly rename-watch --config "./rename-watch.json" --check-config

This validates the schema and relative paths, creates a missing watch root, probes watch-root, destination, optional-quarantine, and runtime-state access, strictly reads existing counter and recovery state, and verifies that every watch-root lock is available. The access checks use disposable create, flush, atomic-replace, and delete probes and restore destination, quarantine, and state directories that did not exist before the check. They do not start observers, recover operations, move user files, consume counters, or write audit records.

Preview the frozen --once plan without moving files or consuming counter state:

indexly rename-watch --config "./rename-watch.json" --once --dry-run

Each output line identifies the job, source, and proposed destination. For an exact-name collision configured as quarantine or leave-source, it also reports that non-moving disposition. Preview uses the same deterministic source order as a real --once run, models persisted counters and existing/planned collisions in memory, and refuses to continue when recovery state is unfinished or malformed. When jobs share a watch root, the first job in configuration order reserves each source, matching the order in which a real --once run can consume it.

To model case and Unicode filename collisions on the destination’s actual filesystem, dry-run briefly creates and removes uniquely named probe files in the nearest existing directory on that volume while holding the watch-root lock. No probe, destination directory, journal, counter update, or audit record is retained, but another filesystem-monitoring tool may observe those brief probe events.

Inspect configured jobs and durable operational state without starting the service:

indexly rename-watch --config "./rename-watch.json" --status
indexly rename-watch --config "./rename-watch.json" --status --json

The human report and versioned JSON document include each configured mode and path, quarantine and collision settings, watch-path availability, pending recovery journals, sanitized durable active failures with their retry IDs, the latest successful move found in retained logs, and the count plus newest ten terminal failures found in retained logs. Durable failures are reported separately from retained history because log rotation never removes actionable failure state. The JSON schema identifier is indexly.rename-watch.status and its current version is 1.

Status is a read-only snapshot. It does not acquire the consumer lock, start an observer or worker, run access or filesystem-policy probes, recover or change a journal, inspect or consume counters, apply log retention, write audit records, or create configured watch, destination, or quarantine directories. A running service’s settling and retry queue exists only in that process, so status reports the live pending queue as unavailable rather than incorrectly reporting zero files.

History is limited to NDJSON files still present under Indexly’s configured log tree. Missing retained events therefore mean “not found in retained logs,” not “never happened.” A malformed, unreadable, or concurrently changing log entry is skipped and makes the snapshot explicitly degraded with structured warnings; the command can still report the remaining retained history. An unsafe, malformed, or unreadable active recovery journal fails the command instead of presenting recovery state as complete. A successful JSON invocation writes exactly one document to standard output.

Monitor a live service

Health, readiness, and metrics read the private atomic runtime snapshot written by the continuous process:

indexly rename-watch --config "./rename-watch.json" --health
indexly rename-watch --config "./rename-watch.json" --readiness --json
indexly rename-watch --config "./rename-watch.json" --metrics --json

The schemas are indexly.rename-watch.health, indexly.rename-watch.readiness, and indexly.rename-watch.metrics, each at version 1. Healthy or ready probes exit 0; an unavailable, stale, unhealthy, or not-ready live service exits 4. Metrics are scoped to the current process and remain separate from generic Indexly audit events.

On a supported stop request, Rename Watch stops accepting new work, marks itself unready, and drains pending settling and retry work under the configured process-wide deadline. Work still pending at the deadline is left at source without a false terminal-failure record. Hybrid and interval jobs rediscover it; an unchanged event-only source may need a controlled --once run, a new filesystem event, or a temporary switch to hybrid mode.

For complete WinSW, systemd, and launchd installation, timeout, permission, logging, upgrade, and rollback procedures, see Operate Rename Watch as a Service.

Inspect and reset counters

Inspect every configured job, or select one exact, case-sensitive job ID:

indexly rename-watch --config "./rename-watch.json" --inspect-counters
indexly rename-watch --config "./rename-watch.json" --inspect-counters --job inbox --json

Inspection is lock-free and read-only. It reports jobs in configuration order with the canonical state namespace, whether the pattern uses {counter}, the storage source (namespaced, legacy, missing, or not_applicable), legacy ambiguity, and sorted date-key allocations. A job without {counter} is marked not_applicable; stale state for that job is not read. The JSON schema is indexly.rename-watch.counters, version 1. Inspection does not start the service, acquire its consumer lock, recover operations, or create runtime directories.

Reset one existing date key or all counter allocations for one job:

indexly rename-watch --config "./rename-watch.json" --reset-counters --job inbox --date-key 20260716
indexly rename-watch --config "./rename-watch.json" --reset-counters --job inbox --all-counters --yes
indexly rename-watch --config "./rename-watch.json" --reset-counters --job inbox --all-counters --yes --json

Reset accepts exactly one counter-enabled job and exactly one of --date-key or --all-counters. Without --yes, an interactive terminal must type the exact phrase RESET <job-id>. Non-interactive use requires --yes, and JSON reset output requires --yes even from an interactive terminal. The reset JSON schema is indexly.rename-watch.counter-reset, version 1.

The command acquires the same watch-root lock as the service, so it refuses to run while that root is being consumed by another rename-watch process. It also fails closed if a recovery journal is pending, malformed, unsafe, or changes during confirmation. Counter state must be a bounded, regular UTF-8 JSON object with supported date keys and non-negative integer values; malformed state is never treated as empty by either the operator or the live allocator.

Before a change, rename-watch flushes an exclusive backup under INDEXLY_HOME/rename-watch/counter-backups/<namespace>/. The backup schema is indexly.rename-watch.counter-backup, version 1, and contains the complete validated pre-reset map. Existing legacy <job-id>.json state is backed up and left unchanged; the reset writes canonical namespaced state, which takes precedence on the next run. Resetting all counters when state is already absent or empty is a no-op and creates neither a backup nor counter state.

A reset changes only the allocator’s next-value floor. It never changes moved files or recovery journals. Existing destination names remain protected by the normal collision checks, so the next move may advance beyond the reset value rather than overwrite a file.

Quarantine, exact-name collisions, and retry

quarantine_subfolder is an optional strict child of watch_path, disjoint from every configured destination and quarantine subtree that overlaps the same job set. When it is omitted, existing configurations keep terminal files at their source path. When it is configured, ordinary failures that exhaust the bounded retry policy are moved there with the same exclusive, identity-checked hard-link/copy fallback used by normal moves. Rename-watch rejects linked or Windows-reparse quarantine components, checks the directory identity throughout transfer, preserves the source on a detected substitution, and never overwrites an existing payload.

Each quarantined file receives a UUID and this layout, preserving its original basename without reserving a filename that an input may legitimately use:

<quarantine_subfolder>/<job-namespace>/<failure-id>/
  payload/<original-basename>
  failure.json

failure.json is immutable, ASCII-safe incident evidence containing the job, original source, attempted destination when known, attempts, timestamps, disposition, and bounded control-free error details. Canonical active state is stored separately under Indexly’s rename-watch/failures/<job-namespace>/ state tree. Startup completes only unambiguous quarantine transitions, repairs a missing sidecar after a finalized payload, and fails closed on detected directory substitution, replacement, linked, partial-copy, or otherwise ambiguous evidence. Failure audit delivery is at least once and is deduplicable by failure_id.

For a pattern without {counter}, no_counter_collision_policy accepts fail, quarantine, or leave-source and defaults to fail. It must not be set on a counter pattern. fail preserves bounded retry behavior; quarantine and leave-source become terminal immediately. The quarantine value requires quarantine_subfolder. No policy appends an undeclared counter or overwrites the exact destination. For example:

Policy Collision behavior Use it when
fail Apply bounded retries, then use the configured terminal-failure destination or leave the source in place. A destination collision may be temporary or should require explicit operator review.
quarantine Record an immediate terminal failure and move the source into its unique quarantine incident directory. Exact output names are mandatory and collisions should leave the intake queue. Requires quarantine_subfolder.
leave-source Record an immediate terminal failure and keep the unchanged source where it arrived. Another process or an operator should resolve the collision in the watch folder.

Exact-name collision configuration:

{
  "pattern": "{date}-{title}",
  "counter_format": "",
  "quarantine_subfolder": ".indexly-quarantine",
  "no_counter_collision_policy": "quarantine"
}

Use the failure IDs shown by --status to retry one failure or a confirmed snapshot of all failures for one job:

indexly rename-watch --config "./rename-watch.json" --retry-failures --job inbox --failure-id 3f7bbf87-842b-4a68-a3a8-1450d36f47f5
indexly rename-watch --config "./rename-watch.json" --retry-failures --job inbox --all-failures --yes
indexly rename-watch --config "./rename-watch.json" --retry-failures --job inbox --all-failures --yes --json

Without --yes, an interactive single retry requires RETRY <failure-id> and a bulk retry requires RETRY ALL <job-id>. Non-interactive, --json, and --json-errors retries require --yes. Successful JSON output uses indexly.rename-watch.failure-retry, version 1.

Retry acquires the normal watch-root lock, recovers safe pending state, verifies that the recorded payload identity and original selection policy still match, and then uses the normal planner. Counter jobs allocate a fresh monotonic counter; exact-name jobs remain exact and no-overwrite. A successful retry removes canonical active state but retains an immutable quarantine sidecar. --all-failures is deterministic and fail-fast: records completed before a later refusal stay completed, while the refused and unattempted records remain durable for another invocation. A finalized normal move with source-deletion trouble is recorded as recovery_pending, never quarantined, and retry resumes its original journaled destination and counter instead of replanning it.

Automation errors and exit codes

Every rename-watch mode accepts --json-errors. The option changes failures only: successful human output remains human, and successful --json status or counter output remains one JSON document on standard output. For automation, combine the options when success and failure must both be structured:

indexly rename-watch --config "./rename-watch.json" --status --json --json-errors

Counter reset and failure retry require --yes whenever --json-errors is present. This prevents an interactive confirmation prompt from contaminating either machine output stream.

On failure, standard output is empty and --json-errors writes exactly one compact, newline-terminated ASCII JSON document to standard error:

{"schema":"indexly.rename-watch.error","version":1,"exit_code":3,"error":{"category":"config_or_safety","message":"Configuration file not found: rename-watch.json"}}

The schema identifier is indexly.rename-watch.error and its current version is 1. The category and numeric status are stable automation fields. The human-readable message is diagnostic and may change between releases; do not parse it.

Exit code Category or report Meaning
0 Success The command completed successfully.
1 internal An unexpected implementation failure occurred.
2 usage Arguments or an option combination were invalid.
3 config_or_safety Configuration, environment, state integrity, locking, recovery, confirmation, or another safety check refused the operation.
4 Health, readiness, or metrics report The live service is unavailable, stale, unhealthy, not ready, or has no available metrics snapshot.
130 interrupted The process received KeyboardInterrupt, normally from Ctrl+C.

Without --json-errors, failures use one ASCII-safe human diagnostic on standard error and the same exit codes. Help remains human text and exits 0. Rename-watch handles its command boundary before automatic update checks, so structured errors cannot be preceded by update notices or generic Indexly error formatting. This isolation does not change other Indexly commands.

Exit code 0 means the command itself completed. For --once, individual files that exhaust their settling or retry policy retain the existing terminal-failure logging behavior; a successful command status does not claim that every file moved. Interactive KeyboardInterrupt or SIGINT drains and retains status 130. SIGTERM from a supported service manager starts the same bounded drain and exits successfully when cleanup completes. A second stop request forces the drain deadline to expire immediately; a manager that later forcibly terminates the process retains its platform-specific status.

Only one rename-watch process can consume a canonical watch root at a time. The service holds a non-blocking operating-system lock for the complete --once or continuous run: a global named mutex on Windows and a fixed /tmp flock namespace on macOS and Linux. Lock identity combines a stable normalized path with filesystem identity so directory recreation and common path aliases cannot silently bypass exclusion. The namespace is independent of INDEXLY_HOME, TEMP, and TMPDIR. A second process exits with a clear lock error. POSIX lock files may remain after shutdown, but they do not represent a stale lock because ownership is enforced by the operating system.

Crash recovery and state

Before consuming a counter or creating a destination, rename-watch writes and flushes a per-operation recovery journal under Indexly’s normal state directory. On restart it acquires the watch-root lock first, then completes unfinished operations before scanning or starting filesystem observers. The recorded destination is reused exactly, so a restart does not consume another counter or silently append a counter to an exact-name pattern.

Recovery automatically resumes the exact target when no destination was created and accepts a source-missing move only after a durable destination-finalized record. If a verified hard-link or copy reached that finalized phase but source deletion was temporarily blocked, recovery retries only the source deletion. A persistent deletion failure emits one terminal record and preserves both paths and the journal for a later run. Other interruptions that leave both paths—including an unfinalized hard link or a partial copy—stop that job with a clear conflict and preserve both paths. Recovery also fails closed when filesystem identities are unavailable, a path was replaced externally, or a configured destination component becomes a symlink or Windows reparse point; it never deletes a pre-existing destination. Keep the journal in place while investigating such a conflict. Changing or deleting it removes the evidence needed for safe recovery.

Successful move audit entries include a stable operation_id. New move and terminal-failure entries also include a namespace derived from the canonical watch root and job ID, preventing retained history from being attributed to a different configuration that later reuses the same job ID. New entries use timezone-aware UTC timestamps; status remains compatible with older retained entries and identifies legacy path-based attribution as ambiguous. Audit delivery is at least once: a sudden stop after the NDJSON append but before the separate journal update can repeat the event after restart, and consumers can deduplicate successful moves by operation_id. A journal is removed only after the audit append succeeds.

Counter and journal filenames use a hash of the canonical watch root and job ID. This avoids raw job IDs becoming path components and keeps jobs with the same ID but different roots independent. Existing safe <job-id>.json counter files remain readable and are migrated to the hashed filename on the next counter update. Patterns without {counter} still neither read nor change counter state.

Portability checks

The repository runs the focused rename-watch and rename compatibility suites on GitHub-hosted Windows, macOS, and Linux. Package installation supplies only Indexly’s declared runtime dependencies; pytest is installed separately from the CI-only requirements-rename-watch-ci.txt and is not included in Indexly’s runtime dependencies or generated Homebrew resources. This workflow is advisory: setup, import, or pytest failures produce warnings and upload a JUnit report when one is available, but do not fail or depend on release and Homebrew publishing workflows. These portability checks do not replace the authoritative macOS Homebrew formula audit.

Naming configuration

pattern is fully configurable. It accepts these placeholders:

Placeholder Meaning Related setting Default behavior
{date} A supported leading filename date, or the file modification date date_format %Y%m%d
{title} Filename without its extension title_format standard
{counter} Per-job, per-date sequence number counter_format Required when used
{prefix} Reserved empty prefix token None Produces no text
Setting Accepted values Rule
date_format %Y%m%d, %Y-%m-%d, %y%m%d, %d-%m-%Y, %d%m%Y Used only when {date} is in the pattern.
counter_format Python integer format such as 03d, or "" Provide a non-empty value only when the pattern contains {counter}. Omit it or use "" otherwise.
title_format standard, camel-case standard preserves the current lowercase kebab-case form (Monthly Reportmonthly-report); camel-case produces monthlyReport.

A pattern without {counter} is valid, for example "{date}-{title}". Its names are exact: if a destination with the same name already exists, rename-watch does not add a counter automatically and does not overwrite the file. Use {counter} when duplicate filenames need automatic numbering. Persisted counter state is ignored and left unchanged whenever the configured pattern does not contain {counter}.

With title_format: "standard", rename-watch uses the same low-level naming rules as rename-file. A supported date already at the start of a filename is preserved and removed from the title portion instead of being duplicated.

See also