Occasionally by mistake I end up mving (moving) my main organisation directories out of my home directory into some random sub directory. It is fairly easily done accidentally with:

cd
mv FILE * DESTINATION/

which in turn is fairly easy to enter if the tab completion on the thing you were intending to move turns out to complete to exactly one file (and thus not need the *), and so tab-expansion automatically adds a space in: if you do not realise before hitting return some of your files/directories vanish inside that destination directory.

After doing this a few times I noticed that certain directories are not affected. Obviously the top directory of wherever I am moving into is not affected, eg:

mv: rename misc to misc/foo/bar/misc: Invalid argument

because that would create a cycle. But much more interestingly most of the "default" directories (ie, created for each user) are also not moved, for a different reason:

mv: rename Desktop to misc/foo/bar/Desktop: Permission denied

I wanted to replicate that behaviour for my own directories in my home directory and went looking for the solution.

This StackExchange question about permission denied gives a hint to what is happening. In particular some of the directories have a + or @ on the end of their permissions mask in a ls -la output. For instance:

ewen@ashram:~$ ls -ald Desktop/
drwxr-xr-x+ 9 ewen  _lpoperator  306 28 May 14:02 Desktop/
ewen@ashram:~$

And looking closely, all of the directories which cannot be moved have similar symbols. Library has an @:

ewen@ashram:~$ ls -ald Library
drwx------@ 55 ewen  staff  1870  9 May 10:21 Library
ewen@ashram:~$

Those markers (+, @) indicate that there are additional attributes or permissions fields on the files; and the @ seems to supercede the +, so an item showing @ may also have attributes that would warrant the + being displayed. The ls(1) manpage explains:

If the file or directory has extended attributes, the permissions
field printed by the -l option is followed by a '@' character.
Otherwise, if the file or directory has extended security information
(such as an access control list), the permissions field printed by
the -l option is followed by a '+' character.

To see the extended attributes, use the "-O" flag to ls, eg:

ewen@ashram:~$ ls -Old Library
drwx------@ 55 ewen  staff  hidden 1870  9 May 10:21 Library
ewen@ashram:~$

which will show an additional column with "file flags" in the long output; this is an OS X extension I think.

To see the access control list (ACL) use the "-e" flag to ls, eg:

ewen@ashram:~$ ls -eld Desktop/
drwxr-xr-x+ 9 ewen  _lpoperator  306 28 May 14:02 Desktop/
 0: group:everyone deny delete
ewen@ashram:~$ ls -led Library/
drwx------@ 55 ewen  staff  1870  9 May 10:21 Library/
 0: group:everyone deny delete
ewen@ashram:~$

(It is also possible to use xattr to show more detail on the "@" attributes, down to their binary coding; but at least in older versions of OS X, such as Mavericks/10.9, there is no getfacl or setfacl -- unlike FreeBSD, which has both getfacl(1) and setfacl(1). However it appears later versions of OS X do have these utilities.)

As a comment on that StackExchange question explains the "deny delete" ACL "also prevents renaming". So clearly it is the ACL that I want to add to my own directories to give them similar protection.

This blog post on ACLs on Mac OS X explains that on OS X the ACLs are managed with the chmod command, and a comment on this StackExchange question about matching permissions gives a function for copying the ACLs -- basically using ls -eld to fetch the ACL, and chmod -E to apply it to the target. According to the chmod(1) manpage, on OS X the chmod -E command:

 -E      Reads the ACL information from stdin, as a sequential list of
         ACEs, separated by newlines.  If the information parses cor-
         rectly, the existing information is replaced.

so will replace the existing ACL information with the new ACL information.

So we can fetch the ACL that we need with:

ls -led Movies | sed -En '/^ [0-9]+: / { s/^ [0-9]+: //; p; }'

(thank to the StackExchange answer for hints to that regex; rewritten to make it more "markdown friendly") which tells us that we need:

group:everyone deny delete

and then apply that to the item that we want to protect with:

echo "group:everyone deny delete" | chmod -E TARGET1 # [TARGET2 ...]

For certainty, we can then check the targets with:

ls -led TARGET1 # [TARGET2 ...]

to make sure they ACLs were updated.

Finally we can verify that we get the protection against accidental movements that we desire with:

ewen@ashram:~$ mv work misc/notes/
mv: rename work to misc/notes/work: Permission denied
ewen@ashram:~$

This does mean that if there is ever a legitimate reason to move those items, we will have to remove the ACL. But it is a very useful addition to items that should always be in that directory. (It may also mean that the normal editor update cycle -- write new, mv new over old -- will not work; so this ACL trick is probably best reserved for directories rather than frequently edited files.)