Skip to content

Improve savepoint input hygiene and direct save flags#25

Merged
dd3ok merged 2 commits into
mainfrom
codex/savepoint-input-hygiene-direct-flags
Jun 12, 2026
Merged

Improve savepoint input hygiene and direct save flags#25
dd3ok merged 2 commits into
mainfrom
codex/savepoint-input-hygiene-direct-flags

Conversation

@dd3ok

@dd3ok dd3ok commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Scan semantic input JSON with --scan-redaction before rendering, without printing matched secret values.
  • Add direct save flags for simple savepoints and safe --delete-input-on-success cleanup under .savepoint/.
  • Add check-output-contract.py, wire it into validation, and document migration/input/Windows notes in README and references.

Validation

  • python3 scripts/check-frontmatter.py
  • python3 scripts/check-marker-block.py
  • python3 scripts/check-marker-semantics.py
  • python3 scripts/validate-examples.py
  • python3 scripts/check-output-contract.py
  • python3 scripts/validate-repo.py
  • python3 scripts/check-savepoint-renderer.py
  • python3 scripts/check-install-helper.py (Windows symlink case skipped due local privilege)
  • python3 scripts/savepoint.py validate --allow-example-paths examples/SAVEPOINT.filled.example.md examples/file-bugfix/SAVEPOINT.md examples/file-architecture/SAVEPOINT.md examples/unsafe-savepoint/SAVEPOINT.md
  • python3 -m compileall -q skills/savepoint/scripts scripts
  • git diff --check

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a new validation script check-output-contract.py to verify the output contract JSON, adds support for direct CLI flags in savepoint.py as an alternative to input JSON files, and implements pre-render redaction scanning along with automatic input deletion on success. Feedback on these changes highlights opportunities to improve the robustness of path validation in is_under_savepoint_dir to safely handle symlinks and uncaught OSError exceptions, catch broader OSError exceptions during contract validation, and correct a minor typo in an error message.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread skills/savepoint/scripts/savepoint.py Outdated
Comment on lines +211 to +218
def is_under_savepoint_dir(path: Path) -> bool:
base = (Path.cwd() / ".savepoint").resolve()
absolute = path if path.is_absolute() else Path.cwd() / path
try:
absolute.resolve().relative_to(base)
except ValueError:
return False
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The path validation in is_under_savepoint_dir resolves the input path using absolute.resolve() before checking if it is under .savepoint/. If the input path is a symlink located outside .savepoint/ that points to a file inside .savepoint/, absolute.resolve() will resolve to the path inside .savepoint/, causing the function to return True. Consequently, delete_input_on_success will delete the symlink located outside .savepoint/, violating the safety guarantee.\n\nAdditionally, absolute.resolve() can raise an OSError (e.g., due to permission issues or infinite symlink loops), which is currently uncaught and could crash the CLI.\n\nTo make this robust and secure:\n1. Catch OSError along with ValueError.\n2. Verify that the parent directory of the unresolved path is under .savepoint/ (by resolving absolute.parent) to ensure the symlink/file itself resides inside .savepoint/.\n3. Ensure the resolved path is a file and also resides under .savepoint/.

def is_under_savepoint_dir(path: Path) -> bool:\n    base = (Path.cwd() / ".savepoint").resolve()\n    absolute = path if path.is_absolute() else Path.cwd() / path\n    try:\n        parent_resolved = absolute.parent.resolve()\n        parent_resolved.relative_to(base)\n        resolved = absolute.resolve()\n        resolved.relative_to(base)\n        return resolved.is_file()\n    except (ValueError, OSError):\n        return False

Comment on lines +34 to +41
def validate_contract(path: Path) -> list[str]:
errors: list[str] = []
try:
data = json.loads(path.read_text(encoding="utf-8-sig"))
except FileNotFoundError:
return [f"{path}: file does not exist"]
except json.JSONDecodeError as exc:
return [f"{path}: invalid JSON: {exc}"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In validate_contract, only FileNotFoundError and json.JSONDecodeError are caught when reading the contract file. If any other OSError occurs (such as PermissionError or IsADirectoryError), the script will crash with an unhandled exception instead of returning a clean list of errors.\n\nCatching OSError instead of FileNotFoundError makes the validation script much more robust.

def validate_contract(path: Path) -> list[str]:\n    errors: list[str] = []\n    try:\n        data = json.loads(path.read_text(encoding="utf-8-sig"))\n    except OSError as exc:\n        return [f"{path}: failed to read file: {exc}"]\n    except json.JSONDecodeError as exc:\n        return [f"{path}: invalid JSON: {exc}"]

Comment on lines +85 to +86
elif any(not isinstance(item, str) or not item.strip() for item in must):
errors.append(f"{path}: case #{index} must entries must be non-empty strings")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

There is a minor typo/grammatical issue in the error message on line 86: "must entries must be". It should be clarified to refer to the 'must' field/entries specifically.

    elif any(not isinstance(item, str) or not item.strip() for item in must):\n        errors.append(f"{path}: case #{index} 'must' entries must be non-empty strings")

@dd3ok dd3ok marked this pull request as ready for review June 12, 2026 10:47
@dd3ok dd3ok merged commit 730c9b9 into main Jun 12, 2026
2 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.

1 participant