shutil.copy2 and shutil.copytree both preserve source-file mode bits. When bundled skills are sourced from a read-only filesystem — the Nix store (mode 0444/0555), squashfs, or an OCI image layer — the user copy in ~/.hermes/skills/ inherits those bits. Later edits via skill_manage, the curator, or even the shutil.rmtree(*.bak) cleanup at the end of an update then fail with PermissionError (silently swallowed by ignore_errors=True, so stale *.bak directories accumulate). Three helpers fix the root cause at the copy boundary: - _ensure_owner_writable(path) — adds the owner-write bit on a single file or directory without touching other mode bits (skips symlinks: os.chmod follows them by default, and a copied symlink pointing outside the profile/tree must not have its external target mutated). - _copy_file_writable(src, dst) — drop-in shutil.copy2 replacement, also used as copytree's copy_function. - _copytree_writable(src, dst) — drop-in shutil.copytree replacement; sweeps the destination tree afterwards since copytree reapplies source-directory metadata after file copies. Wired into every copy site that may read from a read-only bundled source: tools/skills_sync.py (sync_skills new + update paths, restore_official_optional_skill, the DESCRIPTION.md copy, and the reset_bundled_skill rmtree), hermes_cli/profiles.py (--clone and --clone-all), and hermes_cli/profile_distribution.py (apply_distribution). Also repairs two states the copy-boundary fix alone doesn't reach: - Migration for existing installs: a user copy that predates this fix can be hash-identical to the bundled source, so sync_skills takes the "unchanged" no-op path and would never repair it. Both the v1-migration branch and the "bundled unchanged, user unchanged" branch now sweep _make_tree_owner_writable(dest) unconditionally. - Symlink safety in the profile-clone repair sweep: profiles.py clones skills with shutil.copytree(..., symlinks=True, ...), so a skill that is itself a symlink to a shared/vendored directory outside the profile reaches the writable-mode repair as a real symlink entry. _ensure_owner_writable skips symlinks rather than chmod-ing through them into whatever they still point at. Salvages closed PR #20135 (closed by author, not maintainer-rejected) and extends its coverage. Complements #34860 ( |
||
|---|---|---|
| .. | ||
| dashboard_auth | ||
| observability | ||
| proxy | ||
| subcommands | ||
| web_routers | ||
| __init__.py | ||
| _early_recovery.py | ||
| _parser.py | ||
| _scan_venv_blockers.py | ||
| _startup_fast.py | ||
| _subprocess_compat.py | ||
| active_sessions.py | ||
| agent_import.py | ||
| approval_mode.py | ||
| approvals_suggest.py | ||
| auth.py | ||
| auth_commands.py | ||
| azure_detect.py | ||
| backup.py | ||
| bang_shell.py | ||
| banner.py | ||
| blueprint_cmd.py | ||
| browser_connect.py | ||
| build_info.py | ||
| bundles.py | ||
| callbacks.py | ||
| checkpoints.py | ||
| claw.py | ||
| cli_agent_setup_mixin.py | ||
| cli_billing_mixin.py | ||
| cli_commands_mixin.py | ||
| cli_output.py | ||
| clipboard.py | ||
| codex_models.py | ||
| codex_runtime_plugin_migration.py | ||
| codex_runtime_switch.py | ||
| colors.py | ||
| commands.py | ||
| completion.py | ||
| config.py | ||
| config_defaults.py | ||
| config_migrations.py | ||
| console_engine.py | ||
| container_boot.py | ||
| context_switch_guard.py | ||
| copilot_auth.py | ||
| credential_lifecycle.py | ||
| cron.py | ||
| curator.py | ||
| curses_ui.py | ||
| dashboard_procs.py | ||
| dashboard_register.py | ||
| debug.py | ||
| default_soul.py | ||
| dep_ensure.py | ||
| diagnostics_upload.py | ||
| dingtalk_auth.py | ||
| doctor.py | ||
| dump.py | ||
| env_loader.py | ||
| fallback_cmd.py | ||
| fallback_config.py | ||
| focus_view.py | ||
| gateway.py | ||
| gateway_enroll.py | ||
| gateway_windows.py | ||
| goals.py | ||
| gui_uninstall.py | ||
| hooks.py | ||
| init_command.py | ||
| input_sanitize.py | ||
| inventory.py | ||
| journey.py | ||
| kanban.py | ||
| kanban_db.py | ||
| kanban_decompose.py | ||
| kanban_diagnostics.py | ||
| kanban_specify.py | ||
| kanban_swarm.py | ||
| lifecycle.py | ||
| logs.py | ||
| main.py | ||
| managed_scope.py | ||
| managed_uv.py | ||
| mcp_catalog.py | ||
| mcp_config.py | ||
| mcp_picker.py | ||
| mcp_security.py | ||
| mcp_startup.py | ||
| mem_trim.py | ||
| memory_oauth.py | ||
| memory_setup.py | ||
| middleware.py | ||
| migrate.py | ||
| moa_cmd.py | ||
| moa_config.py | ||
| model_catalog.py | ||
| model_cost_guard.py | ||
| model_normalize.py | ||
| model_search.py | ||
| model_setup_flows.py | ||
| model_switch.py | ||
| models.py | ||
| nous_account.py | ||
| nous_auth_keepalive.py | ||
| nous_billing.py | ||
| nous_subscription.py | ||
| npm_engine.py | ||
| onepassword_secrets_cli.py | ||
| oneshot.py | ||
| pairing.py | ||
| partial_compress.py | ||
| pets.py | ||
| platforms.py | ||
| plugins.py | ||
| plugins_cmd.py | ||
| portal_cli.py | ||
| profile_describer.py | ||
| profile_distribution.py | ||
| profiles.py | ||
| projects_cmd.py | ||
| projects_db.py | ||
| prompt_size.py | ||
| prompt_stash.py | ||
| provider_catalog.py | ||
| providers.py | ||
| proxy_cli.py | ||
| psutil_android.py | ||
| pt_input_extras.py | ||
| pty_bridge.py | ||
| pty_session.py | ||
| relaunch.py | ||
| route_identity.py | ||
| runtime_provider.py | ||
| secret_prompt.py | ||
| secrets_cli.py | ||
| security_advisories.py | ||
| security_audit.py | ||
| security_audit_startup.py | ||
| send_cmd.py | ||
| service_manager.py | ||
| session_export.py | ||
| session_export_html.py | ||
| session_export_md.py | ||
| session_filters.py | ||
| session_listing.py | ||
| session_recap.py | ||
| session_recovery.py | ||
| sessions_cmd.py | ||
| setup.py | ||
| setup_hidden_env.py | ||
| setup_whatsapp_cloud.py | ||
| skills_config.py | ||
| skills_hub.py | ||
| skin_cmd.py | ||
| skin_engine.py | ||
| slack_cli.py | ||
| slash_exec.py | ||
| sqlite_runtime.py | ||
| sqlite_safe_read.py | ||
| sqlite_util.py | ||
| status.py | ||
| stdio.py | ||
| suggestions_cmd.py | ||
| telegram_managed_bot.py | ||
| timefmt.py | ||
| timeouts.py | ||
| tips.py | ||
| tools_config.py | ||
| toolset_validation.py | ||
| uninstall.py | ||
| update_cmd.py | ||
| update_lock.py | ||
| urllib_security.py | ||
| vercel_auth.py | ||
| voice.py | ||
| web_deps.py | ||
| web_git.py | ||
| web_models.py | ||
| web_server.py | ||
| webhook.py | ||
| win_pty_bridge.py | ||
| windows_ssh_runtime.py | ||
| write_approval_commands.py | ||
| xai_retirement.py | ||